diff -Nru twisted-conch-0.8.0/bin/tkconch twisted-conch-8.2.0/bin/tkconch --- twisted-conch-0.8.0/bin/tkconch 2004-08-25 09:55:10.000000000 +0100 +++ twisted-conch-8.2.0/bin/tkconch 2008-08-04 18:54:13.000000000 +0100 @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/python # Twisted, the Framework of Your Internet # Copyright (c) 2001-2004 Twisted Matrix Laboratories. diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/debian/changelog /tmp/zqzOGlY6tR/twisted-conch-8.2.0/debian/changelog --- twisted-conch-0.8.0/debian/changelog 2009-01-31 16:12:13.000000000 +0000 +++ twisted-conch-8.2.0/debian/changelog 2009-01-31 16:12:13.000000000 +0000 @@ -1,8 +1,26 @@ -twisted-conch (1:0.8.0-1build1) hardy; urgency=low +twisted-conch (1:8.2.0-1ubuntu1) hardy; urgency=low - * Rebuild with recent python-central. + * Switch distribution to hardy. - -- Matthias Klose Mon, 10 Mar 2008 12:47:56 +0000 + -- Esteve Fernandez Sat, 31 Jan 2009 14:52:53 +0100 + +twisted-conch (1:8.2.0-1) unstable; urgency=low + + * New upstream version. + + -- Esteve Fernandez Sat, 31 Jan 2009 14:41:04 +0100 + +twisted-conch (1:8.1.0-1) unstable; urgency=low + + * New upstream version. + + -- Matthias Klose Wed, 28 May 2008 23:34:02 +0200 + +twisted-conch (1:8.0.0-1) unstable; urgency=low + + * New upstream version. + + -- Matthias Klose Sun, 30 Mar 2008 21:22:01 +0200 twisted-conch (1:0.8.0-1) unstable; urgency=low diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/debian/control /tmp/zqzOGlY6tR/twisted-conch-8.2.0/debian/control --- twisted-conch-0.8.0/debian/control 2009-01-31 16:12:13.000000000 +0000 +++ twisted-conch-8.2.0/debian/control 2009-01-31 16:12:13.000000000 +0000 @@ -2,13 +2,14 @@ Section: python Priority: optional Maintainer: Matthias Klose -Build-Depends: debhelper (>= 5.0.37.2), python-central (>= 0.5), python-all-dev, python-twisted-core (>= 2.5.0), patch +Build-Depends: debhelper (>= 5.0.37.2), python-central (>= 0.6.7), python-all-dev, python-twisted-core (>= 8.2), patch +Build-Conflicts: python-setuptools XS-Python-Version: all -Standards-Version: 3.7.2 +Standards-Version: 3.7.3 Package: python-twisted-conch Architecture: all -Depends: ${python:Depends}, python-crypto (>= 2.0.1+dfsg1-1.1), python-twisted-core (>= 2.5) +Depends: ${python:Depends}, python-crypto (>= 2.0.1+dfsg1-1.1), python-twisted-core (>= 8.2) Provides: conch, ${python:Provides} Conflicts: python2.3-twisted (<< 2.2), python2.3-twisted-conch, python2.4-twisted-conch Replaces: python2.3-twisted (<< 2.2), python2.3-twisted-conch, python2.4-twisted-conch diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/debian/rules /tmp/zqzOGlY6tR/twisted-conch-8.2.0/debian/rules --- twisted-conch-0.8.0/debian/rules 2009-01-31 16:12:13.000000000 +0000 +++ twisted-conch-8.2.0/debian/rules 2009-01-31 16:12:13.000000000 +0000 @@ -71,7 +71,6 @@ dh_compress -i -X.py dh_fixperms -i dh_pycentral -i - dh_python -i dh_installdeb -i dh_gencontrol -i dh_md5sums -i diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/examples/demo_draw.tac /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/examples/demo_draw.tac --- twisted-conch-0.8.0/doc/examples/demo_draw.tac 2005-01-09 06:26:21.000000000 +0000 +++ twisted-conch-8.2.0/doc/examples/demo_draw.tac 2008-08-17 19:51:31.000000000 +0100 @@ -1,6 +1,9 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. +# You can run this .tac file directly with: +# twistd -ny demo_draw.tac + """A trivial drawing application. Clients are allowed to connect and spew various characters out over diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/examples/demo_insults.tac /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/examples/demo_insults.tac --- twisted-conch-0.8.0/doc/examples/demo_insults.tac 2005-01-09 06:26:21.000000000 +0000 +++ twisted-conch-8.2.0/doc/examples/demo_insults.tac 2008-08-17 19:51:31.000000000 +0100 @@ -1,6 +1,9 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. +# You can run this .tac file directly with: +# twistd -ny demo_insults.tac + """Various simple terminal manipulations using the insults module. This demo sets up two listening ports: one on 6022 which accepts ssh diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/examples/demo_manhole.tac /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/examples/demo_manhole.tac --- twisted-conch-0.8.0/doc/examples/demo_manhole.tac 2004-12-19 17:48:40.000000000 +0000 +++ twisted-conch-8.2.0/doc/examples/demo_manhole.tac 2008-08-17 19:51:31.000000000 +0100 @@ -1,6 +1,9 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. +# You can run this .tac file directly with: +# twistd -ny demo_manhole.tac + """An interactive Python interpreter with syntax coloring. Nothing interesting is actually defined here. Two listening ports are diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/examples/demo_recvline.tac /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/examples/demo_recvline.tac --- twisted-conch-0.8.0/doc/examples/demo_recvline.tac 2004-12-19 17:48:40.000000000 +0000 +++ twisted-conch-8.2.0/doc/examples/demo_recvline.tac 2008-08-17 19:51:31.000000000 +0100 @@ -1,6 +1,9 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. +# You can run this .tac file directly with: +# twistd -ny demo_recvline.tac + """Demonstrates line-at-a-time handling with basic line-editing support. This is a variation on the echo server. It sets up two listening diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/examples/demo_scroll.tac /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/examples/demo_scroll.tac --- twisted-conch-0.8.0/doc/examples/demo_scroll.tac 2005-10-25 04:12:30.000000000 +0100 +++ twisted-conch-8.2.0/doc/examples/demo_scroll.tac 2008-08-17 19:51:31.000000000 +0100 @@ -1,6 +1,9 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. +# You can run this .tac file directly with: +# twistd -ny demo_scroll.tac + """Simple echo-ish server that uses the scroll-region. This demo sets up two listening ports: one on 6022 which accepts ssh diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/examples/demo.tac /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/examples/demo.tac --- twisted-conch-0.8.0/doc/examples/demo.tac 2004-12-19 17:48:40.000000000 +0000 +++ twisted-conch-8.2.0/doc/examples/demo.tac 2008-08-17 19:51:31.000000000 +0100 @@ -1,6 +1,9 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. +# You can run this .tac file directly with: +# twistd -ny demo.tac + """Nearly pointless demonstration of the manhole interactive interpreter. This does about the same thing as demo_manhole, but uses the tap diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/examples/index.html /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/examples/index.html --- twisted-conch-0.8.0/doc/examples/index.html 2007-01-07 02:41:06.000000000 +0000 +++ twisted-conch-8.2.0/doc/examples/index.html 2008-12-28 18:57:52.000000000 +0000 @@ -1,2 +1,2 @@ Twisted Documentation: Twisted.Conch code examples

Twisted.Conch code examples

Simple server and client

twisted.conch.insults examples

Index

Version: 0.8.0 \ No newline at end of file + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">Twisted Documentation: Twisted.Conch code examples

Twisted.Conch code examples

Simple server and client

twisted.conch.insults examples

Index

Version: 8.2.0 \ No newline at end of file diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/examples/sshsimpleclient.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/examples/sshsimpleclient.py --- twisted-conch-0.8.0/doc/examples/sshsimpleclient.py 2005-02-10 00:43:39.000000000 +0000 +++ twisted-conch-8.2.0/doc/examples/sshsimpleclient.py 2008-08-04 18:54:13.000000000 +0100 @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/python from twisted.conch.ssh import transport, userauth, connection, common, keys, channel from twisted.internet import defer, protocol, reactor from twisted.python import log diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/examples/sshsimpleserver.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/examples/sshsimpleserver.py --- twisted-conch-0.8.0/doc/examples/sshsimpleserver.py 2005-01-10 23:49:33.000000000 +0000 +++ twisted-conch-8.2.0/doc/examples/sshsimpleserver.py 2008-08-04 18:54:13.000000000 +0100 @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/python from twisted.cred import portal, checkers from twisted.conch import error, avatar from twisted.conch.checkers import SSHPublicKeyDatabase @@ -89,10 +89,10 @@ class ExampleFactory(factory.SSHFactory): publicKeys = { - 'ssh-rsa': keys.getPublicKeyString(data=publicKey) + 'ssh-rsa': keys.Key.fromString(data=publicKey) } privateKeys = { - 'ssh-rsa': keys.getPrivateKeyObject(data=privateKey) + 'ssh-rsa': keys.Key.fromString(data=privateKey) } services = { 'ssh-userauth': userauth.SSHUserAuthServer, diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/examples/window.tac /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/examples/window.tac --- twisted-conch-0.8.0/doc/examples/window.tac 2005-10-18 23:19:11.000000000 +0100 +++ twisted-conch-8.2.0/doc/examples/window.tac 2008-08-17 19:51:31.000000000 +0100 @@ -1,3 +1,9 @@ +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. +# See LICENSE for details. + +# You can run this .tac file directly with: +# twistd -ny window.tac + from __future__ import division import string, random @@ -71,11 +77,14 @@ self.window.filthy() self._draw() + def _schedule(self, f): + reactor.callLater(0, f) + def connectionMade(self): self.terminal.eraseDisplay() self.terminal.resetPrivateModes([insults.privateModes.CURSOR_MODE]) - self.window = window.TopWindow(self._draw) + self.window = window.TopWindow(self._draw, self._schedule) self.output = window.TextOutput((15, 1)) self.input = window.TextInput(15, self._setText) self.select1 = window.Selection(map(str, range(100)), self._setText, 10) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/howto/conch_client.html /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/howto/conch_client.html --- twisted-conch-0.8.0/doc/howto/conch_client.html 2007-01-07 02:41:05.000000000 +0000 +++ twisted-conch-8.2.0/doc/howto/conch_client.html 2008-12-28 18:57:52.000000000 +0000 @@ -1,9 +1,9 @@ Twisted Documentation: Writing a client with Twisted.Conch

Writing a client with Twisted.Conch

Introduction

In the original days of computing, rsh/rlogin were used to connect to + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">Twisted Documentation: Writing a client with Twisted.Conch

Writing a client with Twisted.Conch

Introduction

In the original days of computing, rsh/rlogin were used to connect to remote computers and execute commands. These commands had the problem that the passwords and commands were sent in the clear. To solve this problem, the SSH protocol was created. Twisted.Conch implements the -second version of this protocol.

Writing a client

Writing a client with Conch involves sub-classing 4 classes: twisted.conch.ssh.transport.SSHClientTransport, twisted.conch.ssh.userauth.SSHUserAuthClient, twisted.conch.ssh.connection.SSHConnection, and twisted.conch.ssh.channel.SSHChannel. We'll start out +second version of this protocol.

Writing a client

Writing a client with Conch involves sub-classing 4 classes: twisted.conch.ssh.transport.SSHClientTransport, twisted.conch.ssh.userauth.SSHUserAuthClient, twisted.conch.ssh.connection.SSHConnection, and twisted.conch.ssh.channel.SSHChannel. We'll start out with SSHClientTransport because it's the base of the client.

The Transport

 from twisted.conch import error
@@ -27,7 +27,7 @@
 is called with two strings: the public key sent by the server and its
 fingerprint. You should verify the host key the server sends, either
 by checking against a hard-coded value as in the example, or by asking
-the user. verifyHostKey returns a twisted.internet.defer.Deferred which gets a callback
+the user. verifyHostKey returns a twisted.internet.defer.Deferred which gets a callback
 if the host key is valid, or an errback if it is not. Note that in the
 above, replace 'user' with the username you're attempting to ssh with,
 for instance a call to os.getlogin() for the
@@ -128,12 +128,14 @@
 events to the other side. We pass the method self so that it knows to
 send the request for this channel. The 2nd argument of 'exec' tells the
 server that we want to execute a command. The third argument is the data
-that accompanies the request. common.NS encodes
+that accompanies the request. common.NS encodes
 the data as a length-prefixed string, which is how the server expects
 the data. We also say that we want a reply saying that the process has a
 been started. sendRequest() then returns a
-Deferred which we add a callback for.

Once the callback fires, we send the data. SSHChannel supports the -twisted.internet.interface.Transport interface, so +Deferred which we add a callback for.

Once the callback fires, we send the data. SSHChannel supports the +twisted.internet.interface.Transport interface, so it can be given to Protocols to run them over the secure connection. In our case, we just write the data directly. sendEOF() does not follow the interface, but Conch uses it to tell the other side that we will write no @@ -154,7 +156,7 @@ main()

We call connectTCP() to connect to localhost, port 22 (the standard port for ssh), and pass it an instance -of twisted.internet.protocol.ClientFactory. +of twisted.internet.protocol.ClientFactory. This instance has the attribute protocol set to our earlier ClientTransport class. Note that the protocol attribute is set to the class ClientTransport, not an instance of @@ -167,4 +169,4 @@ closed() method.

If you wish to observe the interactions in more detail, adding a call to log.startLogging(sys.stdout, setStdout=0) before the reactor.run() call will send all -logging to stdout.

Index

Version: 0.8.0 \ No newline at end of file +logging to stdout.

Index

Version: 8.2.0 \ No newline at end of file diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/howto/index.html /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/howto/index.html --- twisted-conch-0.8.0/doc/howto/index.html 2007-01-07 02:41:05.000000000 +0000 +++ twisted-conch-8.2.0/doc/howto/index.html 2008-12-28 18:57:52.000000000 +0000 @@ -1,3 +1,3 @@ Twisted Documentation: Twisted Documentation

Twisted Documentation

    Index

    Version: 0.8.0 \ No newline at end of file + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">Twisted Documentation: Twisted Documentation

    Twisted Documentation

      Index

      Version: 8.2.0 \ No newline at end of file diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/index.html /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/index.html --- twisted-conch-0.8.0/doc/index.html 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/doc/index.html 2008-12-28 18:57:52.000000000 +0000 @@ -0,0 +1,4 @@ +Twisted Documentation: Twisted Conch Documentation

      Twisted Conch Documentation

        • Developer guides: documentation on using +Twisted Conch to develop your own applications
        • Examples: short code examples using +Twisted Conch

        Index

        Version: 8.2.0 \ No newline at end of file diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/man/cftp.1 /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/man/cftp.1 --- twisted-conch-0.8.0/doc/man/cftp.1 2005-10-08 07:42:02.000000000 +0100 +++ twisted-conch-8.2.0/doc/man/cftp.1 2008-07-01 15:16:16.000000000 +0100 @@ -83,7 +83,7 @@ .Sh "REPORTING BUGS" Report bugs to \fIhttp://twistedmatrix.com/bugs/\fR .Sh COPYRIGHT -Copyright \(co 2005 Twisted Matrix Laboratories +Copyright \(co 2005-2008 Twisted Matrix Laboratories .br This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/man/cftp-man.html /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/man/cftp-man.html --- twisted-conch-0.8.0/doc/man/cftp-man.html 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/doc/man/cftp-man.html 2008-12-28 18:57:52.000000000 +0000 @@ -0,0 +1,37 @@ +Twisted Documentation: CFTP.1

        CFTP.1

        NAME

        cftp

        SYNOPSIS

        cftp [-B buffer_size][-b command_file][-R num_requests][-s subsystem]

        DESCRIPTION

        cftp is a client for logging into a remote machine and executing commands to send and receive file information. It can wrap a number of file transfer subsystems +

        The options are as follows: +

        -B
        Specifies the default size of the buffer to use for sending and receiving. (Default value: 32768 bytes.) +
        -b
        File to read commands from, '-' for stdin. (Default value: interactive/stdin.) +
        -R
        Number of requests to make before waiting for a reply. +
        -s
        Subsystem/server program to connect to. +

        The following commands are recognised by +cftp : +

        cd path
        Change the remote directory to 'path'. +
        chgrp gidpath
        Change the gid of 'path' to 'gid'. +
        chmod modepath
        Change mode of 'path' to 'mode'. +
        chown uidpath
        Change uid of 'path' to 'uid'. +
        exit
        Disconnect from the server. +
        get remote-path [local-path]
        Get remote file and optionally store it at specified local path. +
        help
        Get a list of available commands. +
        lcd path
        Change local directory to 'path'. +
        lls [ls-options] [path]
        Display local directory listing. +
        lmkdir path
        Create local directory. +
        ln linkpathtargetpath
        Symlink remote file. +
        lpwd
        Print the local working directory. +
        ls [-l] [path]
        Display remote directory listing. +
        mkdir path
        Create remote directory. +
        progress
        Toggle progress bar. +
        put local-path [remote-path]
        Transfer local file to remote location +
        pwd
        Print the remote working directory. +
        quit
        Disconnect from the server. +
        rename oldpathnewpath
        Rename remote file. +
        rmdir path
        Remove remote directory. +
        rm path
        Remove remote file. +
        version
        Print the SFTP version. +
        ?
        Synonym for 'help'. +

        AUTHOR

        cftp by Paul Swartz <z3p@twistedmatrix.com>. Man page by Mary Gardiner <mary@twistedmatrix.com>. +

        REPORTING BUGS

        Report bugs to http://twistedmatrix.com/bugs/

        COPYRIGHT

        Copyright © 2005-2008 Twisted Matrix Laboratories +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +

        Index

        Version: 8.2.0 \ No newline at end of file diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/man/ckeygen.1 /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/man/ckeygen.1 --- twisted-conch-0.8.0/doc/man/ckeygen.1 2005-10-08 07:02:05.000000000 +0100 +++ twisted-conch-8.2.0/doc/man/ckeygen.1 2008-08-10 19:06:13.000000000 +0100 @@ -11,7 +11,7 @@ The \fB\--help\fR prints out a usage message to standard output. .TP \fB-b\fR, \fB--bits\fR -Number of bits in the key to create. +Number of bits in the key to create (default: 1024) .TP \fB-f\fR, \fB--filename\fR Filename of the key file. @@ -38,7 +38,10 @@ Be quiet. .TP \fB-y\fR, \fB--showpub\fR -Show public key. +Read private key file and print public key. +.TP +\fB--version\fR +Display version number only. .SH DESCRIPTION Manipulate public/private keys in various ways. If no filename is given, a file name will be requested interactively. @@ -47,7 +50,7 @@ .SH "REPORTING BUGS" To report a bug, visit \fIhttp://twistedmatrix.com/bugs/\fR .SH COPYRIGHT -Copyright \(co 2002 Matthew W. Lefkowitz +Copyright \(co 2002-2008 Twisted Matrix Laboratories. .br This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/man/ckeygen-man.html /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/man/ckeygen-man.html --- twisted-conch-0.8.0/doc/man/ckeygen-man.html 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/doc/man/ckeygen-man.html 2008-12-28 18:57:52.000000000 +0000 @@ -0,0 +1,22 @@ +Twisted Documentation: CKEYGEN.1

        CKEYGEN.1

        NAME

        ckeygen - connect to SSH servers +

        SYNOPSIS

        ckeygen [-b bits] [-f filename] [-t type][-Ccomment] [-N new passphrase] [-P old passphrase][-l] [-p] [-q] [-y]ckeygen --help

        DESCRIPTION

        The --help prints out a usage message to standard output. +

        -b, --bits<bits>
        Number of bits in the key to create (default: 1024) +
        -f, --filename<file name>
        Filename of the key file. +
        -t, --type<type>
        Type of key (rsa or dsa). +
        -C, --comment<comment>
        Provide a new comment. +
        -N, --newpass<pass phrase>
        Provide new passphrase. +
        -P, --pass<pass phrase>
        Provide old passphrase. +
        -l, --fingerprint
        Show fingerprint of key file. +
        -p, --changepass
        Change passphrase of private key file. +
        -q, --quiet
        Be quiet. +
        -y, --showpub
        Read private key file and print public key. +
        --version
        Display version number only. +

        DESCRIPTION

        Manipulate public/private keys in various ways. +If no filename is given, a file name will be requested interactively. +

        AUTHOR

        Written by Moshe Zadka, based on ckeygen's help messages +

        REPORTING BUGS

        To report a bug, visit http://twistedmatrix.com/bugs/

        COPYRIGHT

        Copyright © 2002-2008 Twisted Matrix Laboratories. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +

        SEE ALSO

        ssh(1), conch(1) +

        Index

        Version: 8.2.0 \ No newline at end of file diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/man/conch.1 /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/man/conch.1 --- twisted-conch-0.8.0/doc/man/conch.1 2009-01-31 16:12:13.000000000 +0000 +++ twisted-conch-8.2.0/doc/man/conch.1 2008-07-01 15:16:16.000000000 +0100 @@ -148,7 +148,7 @@ port .Ar hostport from the remote machine. -Only root can forward privileged ports. +Only root can forward privieged ports. .It Fl l Ar user Log in using this username. .It Fl m Ar mac_spec @@ -198,7 +198,7 @@ .Sh "REPORTING BUGS" To report a bug, visit \fIhttp://twistedmatrix.com/bugs/\fR .Sh COPYRIGHT -Copyright \(co 2002 Matthew W. Lefkowitz +Copyright \(co 2002-2008 Twisted Matrix Laboratories. .br This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/man/conch-man.html /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/man/conch-man.html --- twisted-conch-0.8.0/doc/man/conch-man.html 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/doc/man/conch-man.html 2008-12-28 18:57:52.000000000 +0000 @@ -0,0 +1,81 @@ +Twisted Documentation: CONCH.1

        CONCH.1

        NAME

        conch

        SYNOPSIS

        conch [-AaCfINnrsTtVvx][-c cipher_spec][-e escape_char][-i identity_file][-K connection_spec][-L port: host: hostport][-l user][-m mac_spec][-o openssh_option][-p port][-R port: host: hostport][ user@] hostname[ command]

        DESCRIPTION

        conch is a SSHv2 client for logging into a remote machine and executing commands. It provides encrypted and secure communications across a possibly insecure network. Arbitrary TCP/IP ports can also be forwarded over the secure connection. +

        conch connects and logs into + hostname(as + useror the current username). The user must prove her/his identity through a public-key or a password. Alternatively, if a connection is already open to a server, a new shell can be opened over the connection without having to reauthenticate. +

        If + commandis specified, + commandis executed instead of a shell. If the +-soption is given, + commandis treated as an SSHv2 subsystem name. +Conch supports the public-key, keyboard-interactive, and password authentications. +

        The public-key method allows the RSA or DSA algorithm to be used. The client uses his/her private key, +or +to sign the session identifier, known only by the client and server. The server checks that the matching public key is valid for the user, and that the signature is correct. +

        If public-key authentication fails, +conch can authenticate by sending an encrypted password over the connection. +conch has the ability to multiplex multiple shells, commands and TCP/IP ports over the same secure connection. To disable multiplexing for a connection, use the +-Iflag. +

        The +-Koption determines how the client connects to the remote host. It is a comma-separated list of the methods to use, in order of preference. The two connection methods are +(for connecting over a multiplexed connection) and +(to connect directly). +To disable connecting over a multiplexed connection, do not include +in the preference list. +

        As an example of how connection sharing works, to speed up CVS over SSH: +

        conch --noshell --fork -l cvs_user cvs_host +set CVS_RSH=conch

        Now, when CVS connects to cvs_host as cvs_user, instead of making a new connection to the server, +conch will add a new channel to the existing connection. This saves the cost of repeatedly negotiating the cryptography and authentication. +

        The options are as follows: +

        -A
        Enables authentication agent forwarding. +
        -a
        Disables authentication agent forwarding (default). +
        -C
        Enable compression. +
        -c
        cipher_specSelects encryption algorithms to be used for this connection, as a comma-separated list of ciphers in order of preference. The list that +conch supports is (in order of default preference): aes256-ctr, aes256-cbc, aes192-ctr, aes192-cbc, aes128-ctr, aes128-cbc, cast128-ctr, cast128-cbc, blowfish-ctr, blowfish, idea-ctr, idea-cbc, 3des-ctr, 3des-cbc. +
        -e
        ch| ^ch | noneSets the escape character for sessions with a PTY (default: +The escape character is only recognized at the beginning of a line (after a newline). +The escape character followed by a dot +closes the connection; +followed by ^Z suspends the connection; +and followed by the escape character sends the escape character once. +Setting the character to +disables any escapes. +
        -f
        Fork to background after authentication. +
        -I
        Do not allow connection sharing over this connection. +
        -i
        identity_specThe file from which the identity (private key) for RSA or DSA authentication is read. +The defaults are +and +It is possible to use this option more than once to use more than one private key. +
        -K
        connection_specSelects methods for connection to the server, as a comma-separated list of methods in order of preference. See +for more information. +
        -L
        port: host : hostportSpecifies that the given port on the client host is to be forwarded to the given host and port on the remote side. This allocates a socket to listen to + porton the local side, and when connections are made to that socket, they are forwarded over the secure channel and a connection is made to + hostport + hostportfrom the remote machine. +Only root can forward privieged ports. +
        -l
        userLog in using this username. +
        -m
        mac_specSelects MAC (message authentication code) algorithms, as a comma-separated list in order of preference. The list that +conch supports is (in order of preference): hmac-sha1, hmac-md5. +
        -N
        Do not execute a shell or command. +
        -n
        Redirect input from /dev/null. +
        -o
        openssh_optionIgnored OpenSSH options. +
        -p
        portThe port to connect to on the server. +
        -R
        port: host : hostportSpecifies that the given port on the remote host is to be forwarded to the given host and port on the local side. This allocates a socket to listen to + porton the remote side, and when connections are made to that socket, they are forwarded over the secure channel and a connection is made to + hostport + hostportfrom the client host. +Only root can forward privieged ports. +
        -s
        Reconnect to the server if the connection is lost. +
        -s
        Invoke + command(mandatory) as a SSHv2 subsystem. +
        -T
        Do not allocate a TTY. +
        -t
        Allocate a TTY even if command is given. +
        -V
        Display version number only. +
        -v
        Log to stderr. +
        -x
        Disable X11 connection forwarding (default). +

        AUTHOR

        Written by Paul Swartz <z3p@twistedmatrix.com>. +

        REPORTING BUGS

        To report a bug, visit http://twistedmatrix.com/bugs/

        COPYRIGHT

        Copyright © 2002-2008 Twisted Matrix Laboratories. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +

        SEE ALSO

        ssh(1) +

        Index

        Version: 8.2.0 \ No newline at end of file diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/man/tkconch.1 /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/man/tkconch.1 --- twisted-conch-0.8.0/doc/man/tkconch.1 2005-10-08 07:02:05.000000000 +0100 +++ twisted-conch-8.2.0/doc/man/tkconch.1 2008-08-10 19:06:13.000000000 +0100 @@ -9,11 +9,14 @@ .PP The \fB\--help\fR prints out a usage message to standard output. .TP -\fB-t\fR, \fB--user\fR -User name to use +\fB-l\fR, \fB--user\fR +Log in using this user name. +.TP +\fB-e\fR, \fB--escape\fR +Set escape character; 'none' = disable (default: ~) .TP \fB-i\fR, \fB--identity\fR -Add an identity file. +Add an identity file for public key authentication (default: ~/.ssh/identity) .TP \fB-c\fR, \fB--cipher\fR Cipher algorithm to use. @@ -24,8 +27,11 @@ \fB-p\fR, \fB--port\fR Port to connect to. .TP -\fB-n\fR, \fB--null\fR -Redirect input from /dev/null +\fB-L\fR, \fB--localforward\fR +Forward local port to remote address. +.TP +\fB-R\fR, \fB--remoteforward\fR +Forward remote port to local address. .TP \fB-t\fR, \fB--tty\fR Allocate a tty even if command is given. @@ -37,16 +43,19 @@ Display version number only. .TP \fB-C\fR, \fB--compress\fR -nable compression. +Enable compression. +.TP +\fB-a\fR, \fB--ansilog\fR +Print the received data to stdout. .TP \fB-N\fR, \fB--noshell\fR Do not execute a shell or command. .TP \fB-s\fR, \fB--subsystem\fR -Invoke command (mandatory) as SSH2 subsystem +Invoke command (mandatory) as SSH2 subsystem. .TP \fB--log\fR -Log to stderr +Print the receieved data to stderr. .SH DESCRIPTION Open an SSH connection to specified server, and either run the command given there or open a remote interactive shell. @@ -55,7 +64,7 @@ .SH "REPORTING BUGS" To report a bug, visit \fIhttp://twistedmatrix.com/bugs/\fR .SH COPYRIGHT -Copyright \(co 2002 Matthew W. Lefkowitz +Copyright \(co 2002-2008 Twisted Matrix Laboratories. .br This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/doc/man/tkconch-man.html /tmp/zqzOGlY6tR/twisted-conch-8.2.0/doc/man/tkconch-man.html --- twisted-conch-0.8.0/doc/man/tkconch-man.html 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/doc/man/tkconch-man.html 2008-12-28 18:57:52.000000000 +0000 @@ -0,0 +1,27 @@ +Twisted Documentation: CONCH.1

        CONCH.1

        NAME

        tkconch - connect to SSH servers graphically +

        SYNOPSIS

        conch [-l user] [-i identity [ -i identity ... ]] [-c cipher] [-m MAC] [-p port] [-n] [-t] [-T] [-V] [-C] [-N] [-s] [arg [...]]

        conch --help

        DESCRIPTION

        The --help prints out a usage message to standard output. +

        -l, --user<user>
        Log in using this user name. +
        -e, --escape<escape character>
        Set escape character; 'none' = disable (default: ~) +
        -i, --identity<identity>
        Add an identity file for public key authentication (default: ~/.ssh/identity) +
        -c, --cipher<cipher>
        Cipher algorithm to use. +
        -m, --macs<mac>
        Specify MAC algorithms for protocol version 2. +
        -p, --port<port>
        Port to connect to. +
        -L, --localforward<listen-port:host:port>
        Forward local port to remote address. +
        -R, --remoteforward<listen-port:host:port>
        Forward remote port to local address. +
        -t, --tty
        Allocate a tty even if command is given. +
        -n, --notty
        Do not allocate a tty. +
        -V, --version
        Display version number only. +
        -C, --compress
        Enable compression. +
        -a, --ansilog
        Print the received data to stdout. +
        -N, --noshell
        Do not execute a shell or command. +
        -s, --subsystem
        Invoke command (mandatory) as SSH2 subsystem. +
        --log
        Print the receieved data to stderr. +

        DESCRIPTION

        Open an SSH connection to specified server, and either run the command +given there or open a remote interactive shell. +

        AUTHOR

        Written by Moshe Zadka, based on conch's help messages +

        REPORTING BUGS

        To report a bug, visit http://twistedmatrix.com/bugs/

        COPYRIGHT

        Copyright © 2002-2008 Twisted Matrix Laboratories. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +

        SEE ALSO

        ssh(1) +

        Index

        Version: 8.2.0 \ No newline at end of file diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/LICENSE /tmp/zqzOGlY6tR/twisted-conch-8.2.0/LICENSE --- twisted-conch-0.8.0/LICENSE 2006-06-03 21:59:55.000000000 +0100 +++ twisted-conch-8.2.0/LICENSE 2008-06-09 02:48:39.000000000 +0100 @@ -1,4 +1,4 @@ -Copyright (c) 2001-2006 +Copyright (c) 2001-2008 Allen Short Andrew Bennetts Apple Computer, Inc. @@ -12,10 +12,10 @@ Itamar Shtull-Trauring James Knight Jason A. Mobarak +Jean-Paul Calderone Jonathan Lange Jonathan D. Simms -Jp Calderone -Jürgen Hermann +Jürgen Hermann Kevin Turner Mary Gardiner Matthew Lefkowitz @@ -25,7 +25,12 @@ Pavel Pergamenshchik Ralph Meijer Sean Riley +Software Freedom Conservancy Travis B. Hartwell +Thomas Herve +Eyal Lotem +Antoine Pitrou +Andy Gayton Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/NEWS /tmp/zqzOGlY6tR/twisted-conch-8.2.0/NEWS --- twisted-conch-0.8.0/NEWS 2007-01-06 20:07:06.000000000 +0000 +++ twisted-conch-8.2.0/NEWS 2008-12-20 17:14:52.000000000 +0000 @@ -1,3 +1,87 @@ +Ticket numbers in this file can be looked up by visiting +http://twistedmatrix.com/trac/ticket/ + +Conch 8.2.0 (2008-12-16) +======================== + +Features +-------- + - The type of the protocols instantiated by SSHFactory is now parameterized + (#3443) + +Fixes +----- + - A file descriptor leak has been fixed (#3213, #1789) + - "File Already Exists" errors are now handled more correctly (#3033) + - Handling of CR IAC in TelnetClient is now improved (#3305) + - SSHAgent is no longer completely unusable (#3332) + - The performance of insults.ClientProtocol is now greatly increased by + delivering more than one byte at a time to application code (#3386) + - Manhole and the conch server no longer need to be run as root when not + necessary (#2607) + - The value of FILEXFER_ATTR_ACMODTIME has been corrected (#2902) + - The management of known_hosts and host key verification has been overhauled + (#1376, #1301, #3494, #3496, #1292, #3499) + +Other +----- + - #3193, #1633 + + +8.1.0 (2008-05-18) +================== + +Fixes +----- + - A regression was fixed whereby the publicKeys and privateKeys attributes of + SSHFactory would not be interpreted as strings (#3141) + - The sshsimpleserver.py example had a minor bug fix (#3135) + - The deprecated mktap API is no longer used (#3127) + - An infelicity was fixed whereby a NameError would be raised in certain + circumstances during authentication when a ConchError should have been + (#3154) + - A workaround was added to conch.insults for a bug in gnome-terminal whereby + it would not scroll correctly (#3189) + + +8.0.0 (2008-03-17) +================== + +Features +-------- + - Add DEC private mode manipulation methods to ITerminalTransport. (#2403) + +Fixes +----- + - Parameterize the scheduler function used by the insults TopWindow widget. + This change breaks backwards compatibility in the TopWindow initializer. + (#2413) + - Notify subsystems, like SFTP, of connection close. (#2421) + - Change the process file descriptor "connection lost" code to reverse the + setNonBlocking operation done during initialization. (#2371) + - Change ConsoleManhole to wait for connectionLost notification before + stopping the reactor. (#2123, #2371) + - Make SSHUserAuthServer.ssh_USERAUTH_REQUEST return a Deferred. (#2528) + - Manhole's initializer calls its parent class's initializer with its + namespace argument. (#2587) + - Handle ^C during input line continuation in manhole by updating the prompt + and line buffer correctly. (#2663) + - Make twisted.conch.telnet.Telnet by default reject all attempts to enable + options. (#1967) + - Reduce the number of calls into application code to deliver application-level + data in twisted.conch.telnet.Telnet.dataReceived (#2107) + - Fix definition and management of extended attributes in conch file transfer. + (#3010) + - Fix parsing of OpenSSH-generated RSA keys with differing ASN.1 packing style. + (#3008) + - Fix handling of missing $HOME in twisted.conch.client.unix. (#3061) + +Misc +---- + - #2267, #2378, #2604, #2707, #2341, #2685, #2679, #2912, #2977, #2678, #2709 + #2063, #2847 + + 0.8.0 (2007-01-06) ================== diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/README /tmp/zqzOGlY6tR/twisted-conch-8.2.0/README --- twisted-conch-0.8.0/README 2007-01-06 19:55:11.000000000 +0000 +++ twisted-conch-8.2.0/README 2008-12-17 04:37:54.000000000 +0000 @@ -1,3 +1,4 @@ -Twisted Conch 0.8.0 +Twisted Conch 8.2.0 -Conch depends on Python Crypto extensions. +Conch depends on Python Crypto () +extensions. diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/setup.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/setup.py --- twisted-conch-0.8.0/setup.py 2006-05-13 20:26:09.000000000 +0100 +++ twisted-conch-8.2.0/setup.py 2008-10-26 21:37:04.000000000 +0000 @@ -1,3 +1,6 @@ +# Copyright (c) 2008 Twisted Matrix Laboratories. +# See LICENSE for details. + import sys try: @@ -36,7 +39,6 @@ author="Twisted Matrix Laboratories", author_email="twisted-python@twistedmatrix.com", maintainer="Paul Swartz", - maintainer_email="z3p@twistedmatrix.com", url="http://twistedmatrix.com/trac/wiki/TwistedConch", license="MIT", long_description="""\ diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/checkers.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/checkers.py --- twisted-conch-0.8.0/twisted/conch/checkers.py 2006-07-01 17:08:17.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/checkers.py 2008-09-02 08:12:21.000000000 +0100 @@ -1,4 +1,12 @@ -import os, base64, binascii +# -*- test-case-name: twisted.conch.test.test_checkers -*- +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Provide L{ICredentialsChecker} implementations to be used in Conch protocols. +""" + +import os, base64, binascii, errno try: import pwd except ImportError: @@ -17,14 +25,16 @@ except ImportError: pamauth = None +from zope.interface import implements, providedBy + from twisted.conch import error from twisted.conch.ssh import keys from twisted.cred.checkers import ICredentialsChecker -from twisted.cred.credentials import IUsernamePassword, ISSHPrivateKey, IPluggableAuthenticationModules +from twisted.cred.credentials import IUsernamePassword, ISSHPrivateKey from twisted.cred.error import UnauthorizedLogin, UnhandledCredentials from twisted.internet import defer from twisted.python import failure, reflect, log -from zope import interface +from twisted.python.util import runAsEffectiveUser def verifyCryptedPassword(crypted, pw): if crypted[0] == '$': # md5_crypt encrypted @@ -35,7 +45,7 @@ class UNIXPasswordDatabase: credentialInterfaces = IUsernamePassword, - interface.implements(ICredentialsChecker) + implements(ICredentialsChecker) def requestAvatarId(self, credentials): if pwd: @@ -63,13 +73,13 @@ if verifyCryptedPassword(shadowPass, credentials.password): return defer.succeed(credentials.username) return defer.fail(UnauthorizedLogin()) - + return defer.fail(UnauthorizedLogin()) class SSHPublicKeyDatabase: credentialInterfaces = ISSHPrivateKey, - interface.implements(ICredentialsChecker) + implements(ICredentialsChecker) def requestAvatarId(self, credentials): d = defer.maybeDeferred(self.checkKey, credentials) @@ -95,33 +105,37 @@ return failure.Failure(UnauthorizedLogin()) def checkKey(self, credentials): - sshDir = os.path.expanduser('~%s/.ssh/' % credentials.username) + """ + Retrieve the keys of the user specified by the credentials, and check + if one matches the blob in the credentials. + """ + sshDir = os.path.expanduser( + os.path.join("~", credentials.username, ".ssh")) if sshDir.startswith('~'): # didn't expand - return 0 + return False uid, gid = os.geteuid(), os.getegid() ouid, ogid = pwd.getpwnam(credentials.username)[2:4] - os.setegid(0) - os.seteuid(0) - os.setegid(ogid) - os.seteuid(ouid) for name in ['authorized_keys2', 'authorized_keys']: - if not os.path.exists(sshDir+name): + filename = os.path.join(sshDir, name) + if not os.path.exists(filename): continue - lines = open(sshDir+name).xreadlines() - os.setegid(0) - os.seteuid(0) - os.setegid(gid) - os.seteuid(uid) + try: + lines = open(filename) + except IOError, e: + if e.errno == errno.EACCES: + lines = runAsEffectiveUser(ouid, ogid, open, filename) + else: + raise for l in lines: l2 = l.split() if len(l2) < 2: continue try: if base64.decodestring(l2[1]) == credentials.blob: - return 1 + return True except binascii.Error: continue - return 0 + return False def _ebRequestAvatarId(self, f): if not f.check(UnauthorizedLogin, error.ValidPublicKey): @@ -131,7 +145,7 @@ class SSHProtocolChecker: - interface.implements(ICredentialsChecker) + implements(ICredentialsChecker) checkers = {} @@ -149,7 +163,7 @@ self.checkers[credentialInterface] = checker def requestAvatarId(self, credentials): - ifac = interface.providedBy(credentials) + ifac = providedBy(credentials) for i in ifac: c = self.checkers.get(i) if c is not None: @@ -157,7 +171,7 @@ self._cbGoodAuthentication, credentials) return defer.fail(UnhandledCredentials("No checker for %s" % \ ', '.join(map(reflect.qal, ifac)))) - + def _cbGoodAuthentication(self, avatarId, credentials): if avatarId not in self.successfulCredentials: self.successfulCredentials[avatarId] = [] diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/client/agent.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/client/agent.py --- twisted-conch-0.8.0/twisted/conch/client/agent.py 2004-08-25 09:36:30.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/client/agent.py 2008-07-29 21:13:54.000000000 +0100 @@ -5,9 +5,7 @@ """ Accesses the key agent for user authentication. -This module is unstable. - -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ from twisted.conch.ssh import agent, channel diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/client/default.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/client/default.py --- twisted-conch-0.8.0/twisted/conch/client/default.py 2005-10-28 21:42:00.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/client/default.py 2008-11-16 02:54:52.000000000 +0000 @@ -1,56 +1,82 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# -*- test-case-name: twisted.conch.test.test_knownhosts -*- +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE for details. -# +""" +Various classes and functions for implementing user-interaction in the +command-line conch client. + +You probably shouldn't use anything in this module directly, since it assumes +you are sitting at an interactive terminal. For example, to programmatically +interact with a known_hosts database, use L{twisted.conch.client.knownhosts}. +""" + +from twisted.python import log +from twisted.python.filepath import FilePath + from twisted.conch.error import ConchError -from twisted.conch.ssh import common, keys, userauth, agent +from twisted.conch.ssh import common, keys, userauth from twisted.internet import defer, protocol, reactor -from twisted.python import log -import agent +from twisted.conch.client.knownhosts import KnownHostsFile, ConsoleUI + +from twisted.conch.client import agent import os, sys, base64, getpass +# This name is bound so that the unit tests can use 'patch' to override it. +_open = open + def verifyHostKey(transport, host, pubKey, fingerprint): - goodKey = isInKnownHosts(host, pubKey, transport.factory.options) - if goodKey == 1: # good key - return defer.succeed(1) - elif goodKey == 2: # AAHHHHH changed - return defer.fail(ConchError('changed host key')) - else: - oldout, oldin = sys.stdout, sys.stdin - sys.stdin = sys.stdout = open('/dev/tty','r+') - if host == transport.transport.getPeer().host: - khHost = host - else: - host = '%s (%s)' % (host, - transport.transport.getPeer().host) - khHost = '%s,%s' % (host, - transport.transport.getPeer().host) - keyType = common.getNS(pubKey)[0] - print """The authenticity of host '%s' can't be established. -%s key fingerprint is %s.""" % (host, - {'ssh-dss':'DSA', 'ssh-rsa':'RSA'}[keyType], - fingerprint) - try: - ans = raw_input('Are you sure you want to continue connecting (yes/no)? ') - except KeyboardInterrupt: - return defer.fail(ConchError("^C")) - while ans.lower() not in ('yes', 'no'): - ans = raw_input("Please type 'yes' or 'no': ") - sys.stdout,sys.stdin=oldout,oldin - if ans == 'no': - print 'Host key verification failed.' - return defer.fail(ConchError('bad host key')) - print "Warning: Permanently added '%s' (%s) to the list of known hosts." % (khHost, {'ssh-dss':'DSA', 'ssh-rsa':'RSA'}[keyType]) - known_hosts = open(os.path.expanduser('~/.ssh/known_hosts'), 'r+') - known_hosts.seek(-1, 2) - if known_hosts.read(1) != '\n': - known_hosts.write('\n') - encodedKey = base64.encodestring(pubKey).replace('\n', '') - known_hosts.write('%s %s %s\n' % (khHost, keyType, encodedKey)) - known_hosts.close() - return defer.succeed(1) + """ + Verify a host's key. + + This function is a gross vestige of some bad factoring in the client + internals. The actual implementation, and a better signature of this logic + is in L{KnownHostsFile.verifyHostKey}. This function is not deprecated yet + because the callers have not yet been rehabilitated, but they should + eventually be changed to call that method instead. + + However, this function does perform two functions not implemented by + L{KnownHostsFile.verifyHostKey}. It determines the path to the user's + known_hosts file based on the options (which should really be the options + object's job), and it provides an opener to L{ConsoleUI} which opens + '/dev/tty' so that the user will be prompted on the tty of the process even + if the input and output of the process has been redirected. This latter + part is, somewhat obviously, not portable, but I don't know of a portable + equivalent that could be used. + + @param host: Due to a bug in L{SSHClientTransport.verifyHostKey}, this is + always the dotted-quad IP address of the host being connected to. + @type host: L{str} + + @param transport: the client transport which is attempting to connect to + the given host. + @type transport: L{SSHClientTransport} + + @param fingerprint: the fingerprint of the given public key, in + xx:xx:xx:... format. This is ignored in favor of getting the fingerprint + from the key itself. + @type fingerprint: L{str} + + @param pubKey: The public key of the server being connected to. + @type pubKey: L{str} + + @return: a L{Deferred} which fires with C{1} if the key was successfully + verified, or fails if the key could not be successfully verified. Failure + types may include L{HostKeyChanged}, L{UserRejectedKey}, L{IOError} or + L{KeyboardInterrupt}. + """ + actualHost = transport.factory.options['host'] + actualKey = keys.Key.fromString(pubKey) + kh = KnownHostsFile.fromPath(FilePath( + transport.factory.options['known-hosts'] + or os.path.expanduser("~/.ssh/known_hosts") + )) + ui = ConsoleUI(lambda : _open("/dev/tty", "r+b")) + return kh.verifyHostKey(ui, actualHost, host, actualKey) + + def isInKnownHosts(host, pubKey, options): """checks to see if host is in the known_hosts file for the user. @@ -58,7 +84,7 @@ """ keyType = common.getNS(pubKey)[0] retVal = 0 - + if not options['known-hosts'] and not os.path.exists(os.path.expanduser('~/.ssh/')): print 'Creating ~/.ssh directory...' os.mkdir(os.path.expanduser('~/.ssh')) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/client/direct.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/client/direct.py --- twisted-conch-0.8.0/twisted/conch/client/direct.py 2005-06-19 23:36:17.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/client/direct.py 2008-03-17 18:44:34.000000000 +0000 @@ -1,18 +1,19 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE for details. -# + +import os + from twisted.internet import defer, protocol, reactor from twisted.conch import error from twisted.conch.ssh import transport from twisted.python import log -import unix +from twisted.conch.client import unix + -import os class SSHClientFactory(protocol.ClientFactory): -# noisy = 1 def __init__(self, d, options, verifyHostKey, userAuthObject): self.d = d @@ -20,16 +21,19 @@ self.verifyHostKey = verifyHostKey self.userAuthObject = userAuthObject + def clientConnectionLost(self, connector, reason): if self.options['reconnect']: connector.connect() + def clientConnectionFailed(self, connector, reason): - if not self.d: return - d = self.d - self.d = None + if self.d is None: + return + d, self.d = self.d, None d.errback(reason) + def buildProtocol(self, addr): trans = SSHClientTransport(self) if self.options['ciphers']: @@ -42,68 +46,77 @@ trans.supportedPublicKeys = self.options['host-key-algorithms'] return trans + + class SSHClientTransport(transport.SSHClientTransport): def __init__(self, factory): self.factory = factory self.unixServer = None + def connectionLost(self, reason): - transport.SSHClientTransport.connectionLost(self, reason) if self.unixServer: - self.unixServer.stopListening() + d = self.unixServer.stopListening() self.unixServer = None + else: + d = defer.succeed(None) + d.addCallback(lambda x: + transport.SSHClientTransport.connectionLost(self, reason)) + def receiveError(self, code, desc): - if not self.factory.d: return - d = self.factory.d - self.factory.d = None + if self.factory.d is None: + return + d, self.factory.d = self.factory.d, None d.errback(error.ConchError(desc, code)) + def sendDisconnect(self, code, reason): - if not self.factory.d: return - d = self.factory.d - self.factory.d = None + if self.factory.d is None: + return + d, self.factory.d = self.factory.d, None transport.SSHClientTransport.sendDisconnect(self, code, reason) d.errback(error.ConchError(reason, code)) + def receiveDebug(self, alwaysDisplay, message, lang): log.msg('Received Debug Message: %s' % message) if alwaysDisplay: # XXX what should happen here? print message + def verifyHostKey(self, pubKey, fingerprint): return self.factory.verifyHostKey(self, self.transport.getPeer().host, pubKey, fingerprint) + def setService(self, service): log.msg('setting client server to %s' % service) transport.SSHClientTransport.setService(self, service) - if service.name != 'ssh-userauth' and self.factory.d: - d = self.factory.d - self.factory.d = None - d.callback(None) if service.name == 'ssh-connection': # listen for UNIX if not self.factory.options['nocache']: user = self.factory.userAuthObject.user peer = self.transport.getPeer() filename = os.path.expanduser("~/.conch-%s-%s-%i" % (user, peer.host, peer.port)) + u = unix.SSHUnixServerFactory(service) try: - u = unix.SSHUnixServerFactory(service) - try: - os.unlink(filename) - except OSError: - pass self.unixServer = reactor.listenUNIX(filename, u, mode=0600, wantPID=1) - except Exception, e: - log.msg('error trying to listen on %s' % filename) - log.err(e) + except: + if self.factory.d is not None: + d, self.factory.d = self.factory.d, None + d.errback(None) + if service.name != 'ssh-userauth' and self.factory.d is not None: + d, self.factory.d = self.factory.d, None + d.callback(None) + def connectionSecure(self): self.requestService(self.factory.userAuthObject) + def connect(host, port, options, verifyHostKey, userAuthObject): d = defer.Deferred() factory = SSHClientFactory(d, options, verifyHostKey, userAuthObject) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/client/__init__.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/client/__init__.py --- twisted-conch-0.8.0/twisted/conch/client/__init__.py 2004-08-25 09:36:30.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/client/__init__.py 2008-07-29 21:13:54.000000000 +0100 @@ -5,7 +5,5 @@ """ Client support code for Conch. -This module is unstable. - -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/client/knownhosts.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/client/knownhosts.py --- twisted-conch-0.8.0/twisted/conch/client/knownhosts.py 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/client/knownhosts.py 2008-11-16 02:54:52.000000000 +0000 @@ -0,0 +1,474 @@ +# -*- test-case-name: twisted.conch.test.test_knownhosts -*- +# Copyright (c) 2008 Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +An implementation of the OpenSSH known_hosts database. + +@since: 8.2 +""" + +import sha +from binascii import Error as DecodeError, b2a_base64 + +from zope.interface import implements + +from Crypto.Hash.HMAC import HMAC + +from twisted.python.randbytes import secureRandom + +from twisted.internet import defer + +from twisted.python import log +from twisted.conch.interfaces import IKnownHostEntry +from twisted.conch.error import HostKeyChanged, UserRejectedKey, InvalidEntry +from twisted.conch.ssh.keys import Key, BadKeyError + + +def _b64encode(s): + """ + Encode a binary string as base64 with no trailing newline. + """ + return b2a_base64(s).strip() + + + +def _extractCommon(string): + """ + Extract common elements of base64 keys from an entry in a hosts file. + + @return: a 4-tuple of hostname data (L{str}), ssh key type (L{str}), key + (L{Key}), and comment (L{str} or L{None}). The hostname data is simply the + beginning of the line up to the first occurrence of whitespace. + """ + elements = string.split(None, 2) + if len(elements) != 3: + raise InvalidEntry() + hostnames, keyType, keyAndComment = elements + splitkey = keyAndComment.split(None, 1) + if len(splitkey) == 2: + keyString, comment = splitkey + comment = comment.rstrip("\n") + else: + keyString = splitkey[0] + comment = None + key = Key.fromString(keyString.decode('base64')) + return hostnames, keyType, key, comment + + + +class _BaseEntry(object): + """ + Abstract base of both hashed and non-hashed entry objects, since they + represent keys and key types the same way. + + @ivar keyType: The type of the key; either ssh-dss or ssh-rsa. + @type keyType: L{str} + + @ivar publicKey: The server public key indicated by this line. + @type publicKey: L{twisted.conch.ssh.keys.Key} + + @ivar comment: Trailing garbage after the key line. + @type comment: L{str} + """ + + def __init__(self, keyType, publicKey, comment): + self.keyType = keyType + self.publicKey = publicKey + self.comment = comment + + + def matchesKey(self, keyObject): + """ + Check to see if this entry matches a given key object. + + @type keyObject: L{Key} + + @rtype: bool + """ + return self.publicKey == keyObject + + + +class PlainEntry(_BaseEntry): + """ + A L{PlainEntry} is a representation of a plain-text entry in a known_hosts + file. + + @ivar _hostname: the host-name associated with this entry. + @type _hostname: L{str} + """ + + implements(IKnownHostEntry) + + def __init__(self, hostnames, keyType, publicKey, comment): + self._hostnames = hostnames + super(PlainEntry, self).__init__(keyType, publicKey, comment) + + + def fromString(cls, string): + """ + Parse a plain-text entry in a known_hosts file, and return a + corresponding L{PlainEntry}. + + @param string: a space-separated string formatted like "hostname + key-type base64-key-data comment". + + @type string: L{str} + + @raise DecodeError: if the key is not valid encoded as valid base64. + + @raise InvalidEntry: if the entry does not have the right number of + elements and is therefore invalid. + + @raise BadKeyError: if the key, once decoded from base64, is not + actually an SSH key. + + @return: an IKnownHostEntry representing the hostname and key in the + input line. + + @rtype: L{PlainEntry} + """ + hostnames, keyType, key, comment = _extractCommon(string) + self = cls(hostnames.split(","), keyType, key, comment) + return self + + fromString = classmethod(fromString) + + + def matchesHost(self, hostname): + """ + Check to see if this entry matches a given hostname. + + @type hostname: L{str} + + @rtype: bool + """ + return hostname in self._hostnames + + + def toString(self): + """ + Implement L{IKnownHostEntry.toString} by recording the comma-separated + hostnames, key type, and base-64 encoded key. + """ + fields = [','.join(self._hostnames), + self.keyType, + _b64encode(self.publicKey.blob())] + if self.comment is not None: + fields.append(self.comment) + return ' '.join(fields) + + +class UnparsedEntry(object): + """ + L{UnparsedEntry} is an entry in a L{KnownHostsFile} which can't actually be + parsed; therefore it matches no keys and no hosts. + """ + + implements(IKnownHostEntry) + + def __init__(self, string): + """ + Create an unparsed entry from a line in a known_hosts file which cannot + otherwise be parsed. + """ + self._string = string + + + def matchesHost(self, hostname): + """ + Always returns False. + """ + return False + + + def matchesKey(self, key): + """ + Always returns False. + """ + return False + + + def toString(self): + """ + Returns the input line, without its newline if one was given. + """ + return self._string.rstrip("\n") + + + +def _hmacedString(key, string): + """ + Return the SHA-1 HMAC hash of the given key and string. + """ + hash = HMAC(key, digestmod=sha) + hash.update(string) + return hash.digest() + + + +class HashedEntry(_BaseEntry): + """ + A L{HashedEntry} is a representation of an entry in a known_hosts file + where the hostname has been hashed and salted. + + @ivar _hostSalt: the salt to combine with a hostname for hashing. + + @ivar _hostHash: the hashed representation of the hostname. + + @cvar MAGIC: the 'hash magic' string used to identify a hashed line in a + known_hosts file as opposed to a plaintext one. + """ + + implements(IKnownHostEntry) + + MAGIC = '|1|' + + def __init__(self, hostSalt, hostHash, keyType, publicKey, comment): + self._hostSalt = hostSalt + self._hostHash = hostHash + super(HashedEntry, self).__init__(keyType, publicKey, comment) + + + def fromString(cls, string): + """ + Load a hashed entry from a string representing a line in a known_hosts + file. + + @raise DecodeError: if the key, the hostname, or the is not valid + encoded as valid base64 + + @raise InvalidEntry: if the entry does not have the right number of + elements and is therefore invalid, or the host/hash portion contains + more items than just the host and hash. + + @raise BadKeyError: if the key, once decoded from base64, is not + actually an SSH key. + """ + stuff, keyType, key, comment = _extractCommon(string) + saltAndHash = stuff[len(cls.MAGIC):].split("|") + if len(saltAndHash) != 2: + raise InvalidEntry() + hostSalt, hostHash = saltAndHash + self = cls(hostSalt.decode("base64"), hostHash.decode("base64"), + keyType, key, comment) + return self + + fromString = classmethod(fromString) + + + def matchesHost(self, hostname): + """ + Implement L{IKnownHostEntry.matchesHost} to compare the hash of the + input to the stored hash. + """ + return (_hmacedString(self._hostSalt, hostname) == self._hostHash) + + + def toString(self): + """ + Implement L{IKnownHostEntry.toString} by base64-encoding the salt, host + hash, and key. + """ + fields = [self.MAGIC + '|'.join([_b64encode(self._hostSalt), + _b64encode(self._hostHash)]), + self.keyType, + _b64encode(self.publicKey.blob())] + if self.comment is not None: + fields.append(self.comment) + return ' '.join(fields) + + + +class KnownHostsFile(object): + """ + A structured representation of an OpenSSH-format ~/.ssh/known_hosts file. + + @ivar _entries: a list of L{IKnownHostEntry} providers. + + @ivar _savePath: the L{FilePath} to save new entries to. + """ + + def __init__(self, savePath): + """ + Create a new, empty KnownHostsFile. + + You want to use L{KnownHostsFile.fromPath} to parse one of these. + """ + self._entries = [] + self._savePath = savePath + + + def hasHostKey(self, hostname, key): + """ + @return: True if the given hostname and key are present in this file, + False if they are not. + + @rtype: L{bool} + + @raise HostKeyChanged: if the host key found for the given hostname + does not match the given key. + """ + for lineidx, entry in enumerate(self._entries): + if entry.matchesHost(hostname): + if entry.matchesKey(key): + return True + else: + raise HostKeyChanged(entry, self._savePath, lineidx + 1) + return False + + + def verifyHostKey(self, ui, hostname, ip, key): + """ + Verify the given host key for the given IP and host, asking for + confirmation from, and notifying, the given UI about changes to this + file. + + @param ui: The user interface to request an IP address from. + + @param hostname: The hostname that the user requested to connect to. + + @param ip: The string representation of the IP address that is actually + being connected to. + + @param key: The public key of the server. + + @return: a L{Deferred} that fires with True when the key has been + verified, or fires with an errback when the key either cannot be + verified or has changed. + + @rtype: L{Deferred} + """ + hhk = defer.maybeDeferred(self.hasHostKey, hostname, key) + def gotHasKey(result): + if result: + if not self.hasHostKey(ip, key): + ui.warn("Warning: Permanently added the %s host key for " + "IP address '%s' to the list of known hosts." % + (key.type(), ip)) + self.addHostKey(ip, key) + self.save() + return result + else: + def promptResponse(response): + if response: + self.addHostKey(hostname, key) + self.addHostKey(ip, key) + self.save() + return response + else: + raise UserRejectedKey() + return ui.prompt( + "The authenticity of host '%s (%s)' " + "can't be established.\n" + "RSA key fingerprint is %s.\n" + "Are you sure you want to continue connecting (yes/no)? " % + (hostname, ip, key.fingerprint())).addCallback(promptResponse) + return hhk.addCallback(gotHasKey) + + + def addHostKey(self, hostname, key): + """ + Add a new L{HashedEntry} to the key database. + + Note that you still need to call L{KnownHostsFile.save} if you wish + these changes to be persisted. + + @return: the L{HashedEntry} that was added. + """ + salt = secureRandom(20) + keyType = "ssh-" + key.type().lower() + entry = HashedEntry(salt, _hmacedString(salt, hostname), + keyType, key, None) + self._entries.append(entry) + return entry + + + def save(self): + """ + Save this L{KnownHostsFile} to the path it was loaded from. + """ + p = self._savePath.parent() + if not p.isdir(): + p.makedirs() + self._savePath.setContent('\n'.join( + [entry.toString() for entry in self._entries]) + "\n") + + + def fromPath(cls, path): + """ + @param path: A path object to use for both reading contents from and + later saving to. + + @type path: L{FilePath} + """ + self = cls(path) + try: + fp = path.open() + except IOError: + return self + for line in fp: + if line.startswith(HashedEntry.MAGIC): + entry = HashedEntry.fromString(line) + else: + try: + entry = PlainEntry.fromString(line) + except (DecodeError, InvalidEntry, BadKeyError): + entry = UnparsedEntry(line) + self._entries.append(entry) + return self + + fromPath = classmethod(fromPath) + + +class ConsoleUI(object): + """ + A UI object that can ask true/false questions and post notifications on the + console, to be used during key verification. + + @ivar opener: a no-argument callable which should open a console file-like + object to be used for reading and writing. + """ + + def __init__(self, opener): + self.opener = opener + + + def prompt(self, text): + """ + Write the given text as a prompt to the console output, then read a + result from the console input. + + @return: a L{Deferred} which fires with L{True} when the user answers + 'yes' and L{False} when the user answers 'no'. It may errback if there + were any I/O errors. + """ + d = defer.succeed(None) + def body(ignored): + f = self.opener() + f.write(text) + while True: + answer = f.readline().strip().lower() + if answer == 'yes': + f.close() + return True + elif answer == 'no': + f.close() + return False + else: + f.write("Please type 'yes' or 'no': ") + return d.addCallback(body) + + + def warn(self, text): + """ + Notify the user (non-interactively) of the provided text, by writing it + to the console. + """ + try: + f = self.opener() + f.write(text) + f.close() + except: + log.err() diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/error.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/error.py --- twisted-conch-0.8.0/twisted/conch/error.py 2004-08-25 09:36:30.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/error.py 2008-11-16 02:54:52.000000000 +0000 @@ -1,34 +1,88 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE for details. -# -# Paul, why didn't you check in an error.py? - -"""An error to represent bad things happening in Conch. - -This module is unstable. +""" +An error to represent bad things happening in Conch. -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ + class ConchError(Exception): def __init__(self, value, data = None): Exception.__init__(self, value, data) self.value = value self.data = data + + class NotEnoughAuthentication(Exception): - """This is thrown if the authentication is valid, but is not enough to + """ + This is thrown if the authentication is valid, but is not enough to successfully verify the user. i.e. don't retry this type of authentication, try another one. """ + + class ValidPublicKey(Exception): - """This is thrown during the authentication process if the public key - is valid for the user. + """ + This is thrown during the authentication process if the public key is valid + for the user. """ + + class IgnoreAuthentication(Exception): - """This is thrown to let the UserAuthServer know it doesn't need to handle - the authentication anymore. """ + This is thrown to let the UserAuthServer know it doesn't need to handle the + authentication anymore. + """ + + + +class MissingKeyStoreError(Exception): + """ + Raised if an SSHAgentServer starts receiving data without its factory + providing a keys dict on which to read/write key data. + """ + + + +class UserRejectedKey(Exception): + """ + The user interactively rejected a key. + """ + + + +class InvalidEntry(Exception): + """ + An entry in a known_hosts file could not be interpreted as a valid entry. + """ + + + +class HostKeyChanged(Exception): + """ + The host key of a remote host has changed. + + @ivar offendingEntry: The entry which contains the persistent host key that + disagrees with the given host key. + + @type offendingEntry: L{twisted.conch.interfaces.IKnownHostEntry} + + @ivar path: a reference to the known_hosts file that the offending entry + was loaded from + + @type path: L{twisted.python.filepath.FilePath} + + @ivar lineno: The line number of the offending entry in the given path. + + @type lineno: L{int} + """ + def __init__(self, offendingEntry, path, lineno): + Exception.__init__(self) + self.offendingEntry = offendingEntry + self.path = path + self.lineno = lineno diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/__init__.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/__init__.py --- twisted-conch-0.8.0/twisted/conch/__init__.py 2007-01-07 02:41:06.000000000 +0000 +++ twisted-conch-8.2.0/twisted/conch/__init__.py 2008-07-29 21:13:54.000000000 +0100 @@ -5,13 +5,13 @@ # -"""Twisted.Conch: The Twisted Shell. Terminal emulation, SSHv2 and telnet. - -Currently this contains the SSHv2 implementation, but it may work over other protocols in the future. (i.e. Telnet) +""" +Twisted.Conch: The Twisted Shell. Terminal emulation, SSHv2 and telnet. -This module is unstable. +Currently this contains the SSHv2 implementation, but it may work over other +protocols in the future. (i.e. Telnet) -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ from twisted.conch._version import version diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/insults/helper.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/insults/helper.py --- twisted-conch-0.8.0/twisted/conch/insults/helper.py 2006-06-16 04:34:57.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/insults/helper.py 2008-07-29 21:13:54.000000000 +0100 @@ -2,11 +2,10 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. -"""Partial in-memory terminal emulator - -API Stability: Unstable +""" +Partial in-memory terminal emulator -@author: U{Jp Calderone} +@author: Jp Calderone """ import re, string @@ -90,6 +89,9 @@ # XXX - need to support scroll regions and scroll history class TerminalBuffer(protocol.Protocol): + """ + An in-memory terminal emulator. + """ implements(insults.ITerminalTransport) for keyID in ('UP_ARROW', 'DOWN_ARROW', 'RIGHT_ARROW', 'LEFT_ARROW', @@ -114,19 +116,39 @@ self.reset() def write(self, bytes): - for b in bytes: + """ + Add the given printable bytes to the terminal. + + Line feeds in C{bytes} will be replaced with carriage return / line + feed pairs. + """ + for b in bytes.replace('\n', '\r\n'): self.insertAtCursor(b) def _currentCharacterAttributes(self): return CharacterAttribute(self.activeCharset, **self.graphicRendition) def insertAtCursor(self, b): + """ + Add one byte to the terminal at the cursor and make consequent state + updates. + + If b is a carriage return, move the cursor to the beginning of the + current row. + + If b is a line feed, move the cursor to the next row or scroll down if + the cursor is already in the last row. + + Otherwise, if b is printable, put it at the cursor position (inserting + or overwriting as dictated by the current mode) and move the cursor. + """ if b == '\r': self.x = 0 - elif b == '\n' or self.x >= self.width: - self.x = 0 + elif b == '\n': self._scrollDown() - if b in string.printable and b not in '\r\n': + elif b in string.printable: + if self.x >= self.width: + self.nextLine() ch = (b, self._currentCharacterAttributes()) if self.modes.get(insults.modes.IRM): self.lines[self.y][self.x:self.x] = [ch] @@ -179,7 +201,11 @@ self._scrollUp() def nextLine(self): - self.insertAtCursor('\n') + """ + Update the cursor position attributes and scroll down if appropriate. + """ + self.x = 0 + self._scrollDown() def saveCursor(self): self._savedCursor = (self.x, self.y) @@ -199,6 +225,36 @@ except KeyError: pass + + def setPrivateModes(self, modes): + """ + Enable the given modes. + + Track which modes have been enabled so that the implementations of + other L{insults.ITerminalTransport} methods can be properly implemented + to respect these settings. + + @see: L{resetPrivateModes} + @see: L{insults.ITerminalTransport.setPrivateModes} + """ + for m in modes: + self.privateModes[m] = True + + + def resetPrivateModes(self, modes): + """ + Disable the given modes. + + @see: L{setPrivateModes} + @see: L{insults.ITerminalTransport.resetPrivateModes} + """ + for m in modes: + try: + del self.privateModes[m] + except KeyError: + pass + + def applicationKeypadMode(self): self.keypadMode = 'app' @@ -306,6 +362,9 @@ self.home = insults.Vector(0, 0) self.x = self.y = 0 self.modes = {} + self.privateModes = {} + self.setPrivateModes([insults.privateModes.AUTO_WRAP, + insults.privateModes.CURSOR_MODE]) self.numericKeypad = 'app' self.activeCharset = insults.G0 self.graphicRendition = { diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/insults/insults.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/insults/insults.py --- twisted-conch-0.8.0/twisted/conch/insults/insults.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/insults/insults.py 2008-08-16 13:03:30.000000000 +0100 @@ -2,14 +2,11 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. -"""VT102 terminal manipulation - -API Stability: Unstable - -@author: U{Jp Calderone} """ +VT102 and VT220 terminal manipulation. -import string +@author: Jp Calderone +""" from zope.interface import implements, Interface @@ -129,6 +126,19 @@ """Reset the given modes on the terminal. """ + + def setPrivateModes(modes): + """ + Set the given DEC private modes on the terminal. + """ + + + def resetPrivateModes(modes): + """ + Reset the given DEC private modes on the terminal. + """ + + def applicationKeypadMode(): """Cause keypad to generate control functions. @@ -598,7 +608,7 @@ def nextLine(self): self.cursorPos.x = 0 self.cursorPos.y = min(self.cursorPos.y + 1, self.termSize.y - 1) - self.write('\x1bE') + self.write('\n') def saveCursor(self): self._savedCursorPos = Vector(self.cursorPos.x, self.cursorPos.y) @@ -838,18 +848,35 @@ del self.terminal def dataReceived(self, bytes): + """ + Parse the given data from a terminal server, dispatching to event + handlers defined by C{self.terminal}. + """ + toWrite = [] for b in bytes: if self.state == 'data': if b == '\x1b': + if toWrite: + self.terminal.write(''.join(toWrite)) + del toWrite[:] self.state = 'escaped' elif b == '\x14': + if toWrite: + self.terminal.write(''.join(toWrite)) + del toWrite[:] self.terminal.shiftOut() elif b == '\x15': + if toWrite: + self.terminal.write(''.join(toWrite)) + del toWrite[:] self.terminal.shiftIn() elif b == '\x08': + if toWrite: + self.terminal.write(''.join(toWrite)) + del toWrite[:] self.terminal.cursorBackward() else: - self.terminal.write(b) + toWrite.append(b) elif self.state == 'escaped': fName = self._shorts.get(b) if fName is not None: @@ -882,6 +909,9 @@ self.state = 'data' else: raise ValueError("Illegal state") + if toWrite: + self.terminal.write(''.join(toWrite)) + def _handleControlSequence(self, buf, terminal): f = getattr(self.controlSequenceParser, CST.get(terminal, terminal), None) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/insults/text.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/insults/text.py --- twisted-conch-0.8.0/twisted/conch/insults/text.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/insults/text.py 2008-07-29 21:13:54.000000000 +0100 @@ -2,7 +2,8 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. -"""Character attribute manipulation API +""" +Character attribute manipulation API This module provides a domain-specific language (using Python syntax) for the creation of text with additional display attributes associated @@ -48,9 +49,7 @@ | A.underline[A.fg.red[\"Hello\", -A.underline[\" world\"]]] -API Stability: Unstable - -@author: U{Jp Calderone} +@author: Jp Calderone """ from twisted.conch.insults import helper, insults diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/insults/window.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/insults/window.py --- twisted-conch-0.8.0/twisted/conch/insults/window.py 2005-10-18 23:19:11.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/insults/window.py 2008-07-29 21:13:54.000000000 +0100 @@ -1,10 +1,9 @@ +# -*- test-case-name: twisted.conch.test.test_window -*- """ Simple insults-based widget library -API Stability: 0 - -@author: U{Jp Calderone} +@author: Jp Calderone """ import array @@ -193,17 +192,37 @@ class TopWindow(ContainerWidget): + """ + A top-level container object which provides focus wrap-around and paint + scheduling. + + @ivar painter: A no-argument callable which will be invoked when this + widget needs to be redrawn. + + @ivar scheduler: A one-argument callable which will be invoked with a + no-argument callable and should arrange for it to invoked at some point in + the near future. The no-argument callable will cause this widget and all + its children to be redrawn. It is typically beneficial for the no-argument + callable to be invoked at the end of handling for whatever event is + currently active; for example, it might make sense to call it at the end of + L{twisted.conch.insults.insults.ITerminalProtocol.keystrokeReceived}. + Note, however, that since calls to this may also be made in response to no + apparent event, arrangements should be made for the function to be called + even if an event handler such as C{keystrokeReceived} is not on the call + stack (eg, using C{reactor.callLater} with a short timeout). + """ focused = True - def __init__(self, painter): + def __init__(self, painter, scheduler): ContainerWidget.__init__(self) self.painter = painter + self.scheduler = scheduler _paintCall = None def repaint(self): if self._paintCall is None: - from twisted.internet import reactor - self._paintCall = reactor.callLater(0, self._paint) + self._paintCall = object() + self.scheduler(self._paint) ContainerWidget.repaint(self) def _paint(self): diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/interfaces.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/interfaces.py --- twisted-conch-0.8.0/twisted/conch/interfaces.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/interfaces.py 2008-11-16 02:54:52.000000000 +0000 @@ -1,12 +1,20 @@ -from zope.interface import Interface +# Copyright (c) 2007-2008 Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +This module contains interfaces defined for the L{twisted.conch} package. +""" + +from zope.interface import Interface, Attribute class IConchUser(Interface): - """A user who has been authenticated to Cred through Conch. This is + """ + A user who has been authenticated to Cred through Conch. This is the interface between the SSH connection and the user. - - @ivar conn: The SSHConnection object for this user. """ + conn = Attribute('The SSHConnection object for this user.') + def lookupChannel(channelType, windowSize, maxPacket, data): """ The other side requested a channel of some sort. @@ -32,17 +40,17 @@ The other side requested a subsystem. subsystem is the name of the subsystem being requested. data is any other packet data (often nothing). - + We return a L{Protocol}. """ def gotGlobalRequest(requestType, data): """ A global request was sent from the other side. - + By default, this dispatches to a method 'channel_channelType' with any - non-alphanumerics in the channelType replace with _'s. If it cannot - find a suitable method, it returns an OPEN_UNKNOWN_CHANNEL_TYPE error. + non-alphanumerics in the channelType replace with _'s. If it cannot + find a suitable method, it returns an OPEN_UNKNOWN_CHANNEL_TYPE error. The method is called with arguments of windowSize, maxPacket, data. """ @@ -79,7 +87,7 @@ """ Called when the other side has indicated no more data will be sent. """ - + def closed(): """ Called when the session is closed. @@ -154,7 +162,8 @@ Rename the given file. This method returns when the rename succeeds, or a L{Deferred} that is - called back when it succeeds. + called back when it succeeds. If the rename fails, C{renameFile} will + raise an implementation-dependent exception. @param oldpath: the current location of the file. @param newpath: the new file name. @@ -288,6 +297,49 @@ as a string. """ + + +class IKnownHostEntry(Interface): + """ + A L{IKnownHostEntry} is an entry in an OpenSSH-formatted C{known_hosts} + file. + + @since: 8.2 + """ + + def matchesKey(key): + """ + Return True if this entry matches the given Key object, False + otherwise. + + @param key: The key object to match against. + @type key: L{twisted.conch.ssh.Key} + """ + + + def matchesHost(hostname): + """ + Return True if this entry matches the given hostname, False otherwise. + + Note that this does no name resolution; if you want to match an IP + address, you have to resolve it yourself, and pass it in as a dotted + quad string. + + @param key: The hostname to match against. + @type key: L{str} + """ + + + def toString(): + """ + @return: a serialized string representation of this entry, suitable for + inclusion in a known_hosts file. (Newline not included.) + + @rtype: L{str} + """ + + + class ISFTPFile(Interface): """ This represents an open file on the server. An object adhering to this diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/manhole.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/manhole.py --- twisted-conch-0.8.0/twisted/conch/manhole.py 2006-06-16 04:34:57.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/manhole.py 2008-07-29 21:13:54.000000000 +0100 @@ -1,17 +1,16 @@ # -*- test-case-name: twisted.conch.test.test_manhole -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2007 Twisted Matrix Laboratories. # See LICENSE for details. -"""Line-input oriented interactive interpreter loop. +""" +Line-input oriented interactive interpreter loop. Provides classes for handling Python source input and arbitrary output interactively from a Twisted application. Also included is syntax coloring code with support for VT102 terminals, control code handling (^C, ^D, ^Q), and reasonable handling of Deferreds. -API Stability: Unstable - -@author: U{Jp Calderone} +@author: Jp Calderone """ import code, sys, StringIO, tokenize @@ -150,7 +149,7 @@ namespace = None def __init__(self, namespace=None): - recvline.HistoricRecvLine.__init__(self, namespace) + recvline.HistoricRecvLine.__init__(self) if namespace is not None: self.namespace = namespace.copy() @@ -164,12 +163,19 @@ def handle_INT(self): + """ + Handle ^C as an interrupt keystroke by resetting the current input + variables to their initial state. + """ + self.pn = 0 + self.lineBuffer = [] + self.lineBufferIndex = 0 + self.interpreter.resetBuffer() + self.terminal.nextLine() self.terminal.write("KeyboardInterrupt") self.terminal.nextLine() self.terminal.write(self.ps[self.pn]) - self.lineBuffer = [] - self.lineBufferIndex = 0 def handle_EOF(self): diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/manhole_ssh.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/manhole_ssh.py --- twisted-conch-0.8.0/twisted/conch/manhole_ssh.py 2005-07-16 02:55:15.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/manhole_ssh.py 2008-07-29 21:13:54.000000000 +0100 @@ -2,11 +2,10 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. -"""insults/SSH integration support. - -API Stability: Unstable +""" +insults/SSH integration support. -@author: U{Jp Calderone} +@author: Jp Calderone """ from zope.interface import implements @@ -122,7 +121,7 @@ publicKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEArzJx8OYOnJmzf4tfBEvLi8DVPrJ3/c9k2I/Az64fxjHf9imyRJbixtQhlH9lfNjUIx+4LmrJH5QNRsFporcHDKOTwTTYLh5KmRpslkYHRivcJSkbh/C+BR3utDS555mV' publicKeys = { - 'ssh-rsa' : keys.getPublicKeyString(data = publicKey) + 'ssh-rsa' : keys.Key.fromString(publicKey) } del publicKey @@ -139,7 +138,7 @@ EhQ0wahUTCk1gKA4uPD6TMTChavbh4K63OvbKg== -----END RSA PRIVATE KEY-----""" privateKeys = { - 'ssh-rsa' : keys.getPrivateKeyObject(data = privateKey) + 'ssh-rsa' : keys.Key.fromString(privateKey) } del privateKey diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/manhole_tap.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/manhole_tap.py --- twisted-conch-0.8.0/twisted/conch/manhole_tap.py 2006-07-01 17:08:17.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/manhole_tap.py 2008-07-29 21:13:54.000000000 +0100 @@ -1,11 +1,10 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. -"""TAP plugin for creating telnet- and ssh-accessible manhole servers. - -API Stability: Unstable +""" +TAP plugin for creating telnet- and ssh-accessible manhole servers. -@author: U{Jp Calderone} +@author: Jp Calderone """ from zope.interface import implements diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/mixin.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/mixin.py --- twisted-conch-0.8.0/twisted/conch/mixin.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/mixin.py 2008-07-29 21:13:54.000000000 +0100 @@ -2,14 +2,13 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. -"""Experimental optimization +""" +Experimental optimization This module provides a single mixin class which allows protocols to collapse numerous small writes into a single larger one. -API Stability: Unstable - -@author: U{Jp Calderone} +@author: Jp Calderone """ from twisted.internet import reactor diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/openssh_compat/factory.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/openssh_compat/factory.py --- twisted-conch-0.8.0/twisted/conch/openssh_compat/factory.py 2004-05-22 01:03:05.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/openssh_compat/factory.py 2008-09-02 08:12:21.000000000 +0100 @@ -1,43 +1,73 @@ -from twisted.conch.ssh import keys, factory, common +# -*- test-case-name: twisted.conch.test.test_openssh_compat -*- +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Factory for reading openssh configuration files: public keys, private keys, and +modile file. +""" + +import os, errno + from twisted.python import log -import primes -import os +from twisted.python.util import runAsEffectiveUser + +from twisted.conch.ssh import keys, factory, common +from twisted.conch.openssh_compat import primes + + class OpenSSHFactory(factory.SSHFactory): dataRoot = '/usr/local/etc' moduliRoot = '/usr/local/etc' # for openbsd which puts moduli in a different # directory from keys + + def getPublicKeys(self): + """ + Return the server public keys. + """ ks = {} - for file in os.listdir(self.dataRoot): - if file[:9] == 'ssh_host_' and file[-8:]=='_key.pub': + for filename in os.listdir(self.dataRoot): + if filename[:9] == 'ssh_host_' and filename[-8:]=='_key.pub': try: - k = keys.getPublicKeyString(self.dataRoot+'/'+file) - t = common.getNS(k)[0] + k = keys.Key.fromFile( + os.path.join(self.dataRoot, filename)) + t = common.getNS(k.blob())[0] ks[t] = k except Exception, e: - log.msg('bad public key file %s: %s' % (file,e)) + log.msg('bad public key file %s: %s' % (filename, e)) return ks + + def getPrivateKeys(self): - ks = {} - euid,egid = os.geteuid(), os.getegid() - os.setegid(0) # gain priviledges - os.seteuid(0) - for file in os.listdir(self.dataRoot): - if file[:9] == 'ssh_host_' and file[-4:]=='_key': + """ + Return the server private keys. + """ + privateKeys = {} + for filename in os.listdir(self.dataRoot): + if filename[:9] == 'ssh_host_' and filename[-4:]=='_key': + fullPath = os.path.join(self.dataRoot, filename) try: - k = keys.getPrivateKeyObject(self.dataRoot+'/'+file) - t = keys.objectType(k) - ks[t] = k + key = keys.Key.fromFile(fullPath) + except IOError, e: + if e.errno == errno.EACCES: + # Not allowed, let's switch to root + key = runAsEffectiveUser(0, 0, keys.Key.fromFile, fullPath) + keyType = keys.objectType(key.keyObject) + privateKeys[keyType] = key + else: + raise except Exception, e: - log.msg('bad private key file %s: %s' % (file, e)) - os.setegid(egid) # drop them just as quickily - os.seteuid(euid) - return ks + log.msg('bad private key file %s: %s' % (filename, e)) + else: + keyType = keys.objectType(key.keyObject) + privateKeys[keyType] = key + return privateKeys + def getPrimes(self): try: return primes.parseModuliFile(self.moduliRoot+'/moduli') except IOError: return None - diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/openssh_compat/__init__.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/openssh_compat/__init__.py --- twisted-conch-0.8.0/twisted/conch/openssh_compat/__init__.py 2004-08-25 09:36:30.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/openssh_compat/__init__.py 2008-07-29 21:13:54.000000000 +0100 @@ -3,10 +3,9 @@ # -"""Support for OpenSSH configuration files. - -This module is unstable. +""" +Support for OpenSSH configuration files. -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/openssh_compat/primes.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/openssh_compat/primes.py --- twisted-conch-0.8.0/twisted/conch/openssh_compat/primes.py 2004-08-25 09:36:30.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/openssh_compat/primes.py 2008-07-29 21:13:54.000000000 +0100 @@ -3,11 +3,10 @@ # -"""Parsing for the moduli file, which contains Diffie-Hellman prime groups. - -This module is unstable. +""" +Parsing for the moduli file, which contains Diffie-Hellman prime groups. -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ def parseModuliFile(filename): diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/recvline.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/recvline.py --- twisted-conch-0.8.0/twisted/conch/recvline.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/recvline.py 2008-07-29 21:13:54.000000000 +0100 @@ -2,11 +2,10 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. -"""Basic line editing support. - -API Stability: Unstable +""" +Basic line editing support. -@author: U{Jp Calderone} +@author: Jp Calderone """ import string diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/scripts/ckeygen.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/scripts/ckeygen.py --- twisted-conch-0.8.0/twisted/conch/scripts/ckeygen.py 2005-03-20 10:23:52.000000000 +0000 +++ twisted-conch-8.2.0/twisted/conch/scripts/ckeygen.py 2007-10-21 07:59:08.000000000 +0100 @@ -1,4 +1,4 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2007 Twisted Matrix Laboratories. # See LICENSE for details. # @@ -7,8 +7,8 @@ #""" Implementation module for the `ckeygen` command. #""" -from twisted.conch.ssh import keys, common -from twisted.python import log, usage +from twisted.conch.ssh import keys +from twisted.python import log, usage, randbytes import sys, os, getpass, md5, socket if getpass.getpass == getpass.unix_getpass: @@ -20,7 +20,7 @@ reload(getpass) class GeneralOptions(usage.Options): - synopsis = """Usage: ckeygen [options] + synopsis = """Usage: ckeygen [options] """ optParameters = [['bits', 'b', 1024, 'Number of bits in the key to create.'], @@ -29,7 +29,7 @@ ['comment', 'C', None, 'Provide new comment.'], ['newpass', 'N', None, 'Provide new passphrase.'], ['pass', 'P', None, 'Provide old passphrase']] - + optFlags = [['fingerprint', 'l', 'Show fingerprint of key file.'], ['changepass', 'p', 'Change passphrase of private key file.'], ['quiet', 'q', 'Quiet.'], @@ -79,13 +79,13 @@ def generateRSAkey(options): from Crypto.PublicKey import RSA print 'Generating public/private rsa key pair.' - key = RSA.generate(int(options['bits']), common.entropy.get_bytes) + key = RSA.generate(int(options['bits']), randbytes.secureRandom) _saveKey(key, options) def generateDSAkey(options): from Crypto.PublicKey import DSA print 'Generating public/private dsa key pair.' - key = DSA.generate(int(options['bits']), common.entropy.get_bytes) + key = DSA.generate(int(options['bits']), randbytes.secureRandom) _saveKey(key, options) def printFingerprint(options): @@ -127,7 +127,7 @@ options['newpass'] = p1 open(options['filename'], 'w').write( keys.makePrivateKeyString(key, passphrase=options['newpass'])) - print 'Your identification has been saved with the new passphrase.' + print 'Your identification has been saved with the new passphrase.' def displayPublicKey(options): if not options['filename']: @@ -143,7 +143,7 @@ options['pass'] = getpass.getpass('Enter passphrase: ') key = keys.getPrivateKeyObject(options['filename'], passphrase = options['pass']) print keys.makePublicKeyString(key) - + def _saveKey(key, options): if not options['filename']: kind = keys.objectType(key) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/scripts/tkconch.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/scripts/tkconch.py --- twisted-conch-0.8.0/twisted/conch/scripts/tkconch.py 2006-12-23 08:21:53.000000000 +0000 +++ twisted-conch-8.2.0/twisted/conch/scripts/tkconch.py 2008-02-22 10:13:08.000000000 +0000 @@ -1,4 +1,5 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# -*- test-case-name: twisted.conch.test.test_scripts -*- +# Copyright (c) 2001-2007 Twisted Matrix Laboratories. # See LICENSE for details. # @@ -250,7 +251,7 @@ def deferredAskFrame(question, echo): if frame.callback: - raise "can't ask 2 questions at once!" + raise ValueError("can't ask 2 questions at once!") d = defer.Deferred() resp = [] def gotChar(ch, resp=resp): diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/agent.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/agent.py --- twisted-conch-0.8.0/twisted/conch/ssh/agent.py 2004-08-25 09:36:30.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ssh/agent.py 2008-08-07 17:46:15.000000000 +0100 @@ -1,33 +1,44 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE for details. -# """ -Implements the old SSHv1 key agent protocol. +Implements the SSH v2 key agent protocol. This protocol is documented in the +SSH source code, in the file +U{PROTOCOL.agent}. -This module is unstable. - -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ import struct -from common import NS, getNS -from twisted.conch.error import ConchError + +from twisted.conch.ssh.common import NS, getNS, getMP +from twisted.conch.error import ConchError, MissingKeyStoreError +from twisted.conch.ssh import keys from twisted.internet import defer, protocol + + class SSHAgentClient(protocol.Protocol): - + """ + The client side of the SSH agent protocol. This is equivalent to + ssh-add(1) and can be used with either ssh-agent(1) or the SSHAgentServer + protocol, also in this package. + """ + def __init__(self): self.buf = '' self.deferreds = [] + def dataReceived(self, data): self.buf += data while 1: - if len(self.buf) <= 4: return + if len(self.buf) <= 4: + return packLen = struct.unpack('!L', self.buf[:4])[0] - if len(self.buf) < 4+packLen: return - packet, self.buf = self.buf[4:4+packLen], self.buf[4+packLen:] + if len(self.buf) < 4 + packLen: + return + packet, self.buf = self.buf[4:4 + packLen], self.buf[4 + packLen:] reqType = ord(packet[0]) d = self.deferreds.pop(0) if reqType == AGENT_FAILURE: @@ -37,101 +48,231 @@ else: d.callback(packet) + def sendRequest(self, reqType, data): - pack = struct.pack('!LB',len(data)+1, reqType)+data + pack = struct.pack('!LB',len(data) + 1, reqType) + data self.transport.write(pack) d = defer.Deferred() self.deferreds.append(d) return d + def requestIdentities(self): - return self.sendRequest(AGENTC_REQUEST_IDENTITIES, '').addCallback(self._cbRequestIdentities) + """ + @return: A L{Deferred} which will fire with a list of all keys found in + the SSH agent. The list of keys is comprised of (public key blob, + comment) tuples. + """ + d = self.sendRequest(AGENTC_REQUEST_IDENTITIES, '') + d.addCallback(self._cbRequestIdentities) + return d + def _cbRequestIdentities(self, data): + """ + Unpack a collection of identities into a list of tuples comprised of + public key blobs and comments. + """ if ord(data[0]) != AGENT_IDENTITIES_ANSWER: - return ConchError('unexpected respone: %i' % ord(data[0])) + raise ConchError('unexpected response: %i' % ord(data[0])) numKeys = struct.unpack('!L', data[1:5])[0] keys = [] data = data[5:] for i in range(numKeys): - blobLen = struct.unpack('!L', data[:4])[0] - blob, data = data[4:4+blobLen], data[4+blobLen:] - commLen = struct.unpack('!L', data[:4])[0] - comm, data = data[4:4+commLen], data[4+commLen:] - keys.append((blob, comm)) + blob, data = getNS(data) + comment, data = getNS(data) + keys.append((blob, comment)) return keys + def addIdentity(self, blob, comment = ''): + """ + Add a private key blob to the agent's collection of keys. + """ req = blob req += NS(comment) - co return self.sendRequest(AGENTC_ADD_IDENTITY, req) + def signData(self, blob, data): + """ + Blob is a public key whose private counterpart was previously added to + the running agent. Return a signature for data using that key. + """ req = NS(blob) req += NS(data) req += '\000\000\000\000' # flags return self.sendRequest(AGENTC_SIGN_REQUEST, req).addCallback(self._cbSignData) + def _cbSignData(self, data): - if data[0] != chr(AGENT_SIGN_RESPONSE): - return ConchError('unexpected data: %i' % ord(data[0])) + if ord(data[0]) != AGENT_SIGN_RESPONSE: + raise ConchError('unexpected data: %i' % ord(data[0])) signature = getNS(data[1:])[0] return signature + def removeIdentity(self, blob): + """ + Remove the private key corresponding to the public key in blob from the + running agent. + """ req = NS(blob) return self.sendRequest(AGENTC_REMOVE_IDENTITY, req) + def removeAllIdentities(self): + """ + Remove all keys from the running agent. + """ return self.sendRequest(AGENTC_REMOVE_ALL_IDENTITIES, '') + + class SSHAgentServer(protocol.Protocol): + """ + The server side of the SSH agent protocol. This is equivalent to + ssh-agent(1) and can be used with either ssh-add(1) or the SSHAgentClient + protocol, also in this package. + """ def __init__(self): - self.buf = '' + self.buf = '' + def dataReceived(self, data): self.buf += data while 1: - if len(self.buf) <= 4: return + if len(self.buf) <= 4: + return packLen = struct.unpack('!L', self.buf[:4])[0] - if len(self.buf) < 4+packLen: return - packet, self.buf = self.buf[4:4+packLen], self.buf[4+packLen:] + if len(self.buf) < 4 + packLen: + return + packet, self.buf = self.buf[4:4 + packLen], self.buf[4 + packLen:] reqType = ord(packet[0]) reqName = messages.get(reqType, None) if not reqName: - print 'bad request', reqType - f = getattr(self, 'agentc_%s' % reqName) - f(packet[1:]) + self.sendResponse(AGENT_FAILURE, '') + else: + f = getattr(self, 'agentc_%s' % reqName) + if getattr(self.factory, 'keys', None) is None: + self.sendResponse(AGENT_FAILURE, '') + raise MissingKeyStoreError() + f(packet[1:]) + def sendResponse(self, reqType, data): - pack = struct.pack('!LB', len(data)+1, reqType) + data + pack = struct.pack('!LB', len(data) + 1, reqType) + data self.transport.write(pack) + def agentc_REQUEST_IDENTITIES(self, data): + """ + Return all of the identities that have been added to the server + """ assert data == '' - numKeys = len(self.keys) - s = struct.pack('!L', numKeys) - for k in self.keys: - s += struct.pack('!L', len(k)) + k - s += struct.pack('!L', len(self.keys[k][1])) + self.keys[k][1] - self.sendResponse(AGENT_IDENTITIES_ANSWER, s) + numKeys = len(self.factory.keys) + resp = [] + + resp.append(struct.pack('!L', numKeys)) + for key, comment in self.factory.keys.itervalues(): + resp.append(NS(key.blob())) # yes, wrapped in an NS + resp.append(NS(comment)) + self.sendResponse(AGENT_IDENTITIES_ANSWER, ''.join(resp)) + def agentc_SIGN_REQUEST(self, data): - blob, data = common.getNS(data) - if blob not in self.keys: + """ + Data is a structure with a reference to an already added key object and + some data that the clients wants signed with that key. If the key + object wasn't loaded, return AGENT_FAILURE, else return the signature. + """ + blob, data = getNS(data) + if blob not in self.factory.keys: return self.sendResponse(AGENT_FAILURE, '') - signData, data = common.getNS(data) + signData, data = getNS(data) assert data == '\000\000\000\000' - self.sendResponse(AGENT_SIGN_RESPONSE, common.NS(keys.signData(self.keys[blob][0], signData))) + self.sendResponse(AGENT_SIGN_RESPONSE, NS(self.factory.keys[blob][0].sign(signData))) - def agentc_ADD_IDENTITY(self, data): pass - def agentc_REMOVE_IDENTITY(self, data): pass - def agentc_REMOVE_ALL_IDENTITIES(self, data): pass + def agentc_ADD_IDENTITY(self, data): + """ + Adds a private key to the agent's collection of identities. On + subsequent interactions, the private key can be accessed using only the + corresponding public key. + """ + + # need to pre-read the key data so we can get past it to the comment string + keyType, rest = getNS(data) + if keyType == 'ssh-rsa': + nmp = 6 + elif keyType == 'ssh-dss': + nmp = 5 + else: + raise keys.BadKeyError('unknown blob type: %s' % keyType) + + rest = getMP(rest, nmp)[-1] # ignore the key data for now, we just want the comment + comment, rest = getNS(rest) # the comment, tacked onto the end of the key blob + + k = keys.Key.fromString(data, type='private_blob') # not wrapped in NS here + self.factory.keys[k.blob()] = (k, comment) + self.sendResponse(AGENT_SUCCESS, '') + + + def agentc_REMOVE_IDENTITY(self, data): + """ + Remove a specific key from the agent's collection of identities. + """ + blob, _ = getNS(data) + k = keys.Key.fromString(blob, type='blob') + del self.factory.keys[k.blob()] + self.sendResponse(AGENT_SUCCESS, '') + + + def agentc_REMOVE_ALL_IDENTITIES(self, data): + """ + Remove all keys from the agent's collection of identities. + """ + assert data == '' + self.factory.keys = {} + self.sendResponse(AGENT_SUCCESS, '') + + # v1 messages that we ignore because we don't keep v1 keys + # open-ssh sends both v1 and v2 commands, so we have to + # do no-ops for v1 commands or we'll get "bad request" errors + + def agentc_REQUEST_RSA_IDENTITIES(self, data): + """ + v1 message for listing RSA1 keys; superseded by + agentc_REQUEST_IDENTITIES, which handles different key types. + """ + self.sendResponse(AGENT_RSA_IDENTITIES_ANSWER, struct.pack('!L', 0)) + + + def agentc_REMOVE_RSA_IDENTITY(self, data): + """ + v1 message for removing RSA1 keys; superseded by + agentc_REMOVE_IDENTITY, which handles different key types. + """ + self.sendResponse(AGENT_SUCCESS, '') + + + def agentc_REMOVE_ALL_RSA_IDENTITIES(self, data): + """ + v1 message for removing all RSA1 keys; superseded by + agentc_REMOVE_ALL_IDENTITIES, which handles different key types. + """ + self.sendResponse(AGENT_SUCCESS, '') + + +AGENTC_REQUEST_RSA_IDENTITIES = 1 +AGENT_RSA_IDENTITIES_ANSWER = 2 AGENT_FAILURE = 5 AGENT_SUCCESS = 6 + +AGENTC_REMOVE_RSA_IDENTITY = 8 +AGENTC_REMOVE_ALL_RSA_IDENTITIES = 9 + AGENTC_REQUEST_IDENTITIES = 11 AGENT_IDENTITIES_ANSWER = 12 AGENTC_SIGN_REQUEST = 13 @@ -141,7 +282,7 @@ AGENTC_REMOVE_ALL_IDENTITIES = 19 messages = {} -import agent -for v in dir(agent): - if v.startswith('AGENTC_'): - messages[getattr(agent, v)] = v[7:] +for name, value in locals().copy().items(): + if name[:7] == 'AGENTC_': + messages[value] = name[7:] # doesn't handle doubles + diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/asn1.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/asn1.py --- twisted-conch-0.8.0/twisted/conch/ssh/asn1.py 2004-08-25 09:36:30.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ssh/asn1.py 2008-07-29 21:13:54.000000000 +0100 @@ -3,11 +3,10 @@ # -"""A basic ASN.1 parser to parse private SSH keys. - -This module is unstable. +""" +A basic ASN.1 parser to parse private SSH keys. -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ from Crypto.Util import number @@ -50,7 +49,7 @@ partData = '\x00' + partData partType = INTEGER else: - raise 'unknown type %s' % type(part) + raise ValueError('unknown type %s' % (type(part),)) ret += chr(partType) if len(partData) > 127: diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/channel.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/channel.py --- twisted-conch-0.8.0/twisted/conch/ssh/channel.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ssh/channel.py 2008-07-29 21:13:54.000000000 +0100 @@ -1,21 +1,59 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# -*- test-case-name: twisted.conch.test.test_channel -*- +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE for details. # -"""The parent class for all the SSH Channels. Currently implemented channels +""" +The parent class for all the SSH Channels. Currently implemented channels are session. direct-tcp, and forwarded-tcp. -This module is unstable. - -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ -from twisted.python import log, context +from twisted.python import log +from twisted.internet import interfaces +from zope.interface import implements + class SSHChannel(log.Logger): + """ + A class that represents a multiplexed channel over an SSH connection. + The channel has a local window which is the maximum amount of data it will + receive, and a remote which is the maximum amount of data the remote side + will accept. There is also a maximum packet size for any individual data + packet going each way. + + @ivar name: the name of the channel. + @type name: C{str} + @ivar localWindowSize: the maximum size of the local window in bytes. + @type localWindowSize: C{int} + @ivar localWindowLeft: how many bytes are left in the local window. + @type localWindowLeft: C{int} + @ivar localMaxPacket: the maximum size of packet we will accept in bytes. + @type localMaxPacket: C{int} + @ivar remoteWindowLeft: how many bytes are left in the remote window. + @type remoteWindowLeft: C{int} + @ivar remoteMaxPacket: the maximum size of a packet the remote side will + accept in bytes. + @type remoteMaxPacket: C{int} + @ivar conn: the connection this channel is multiplexed through. + @type conn: L{SSHConnection} + @ivar data: any data to send to the other size when the channel is + requested. + @type data: C{str} + @ivar avatar: an avatar for the logged-in user (if a server channel) + @ivar localClosed: True if we aren't accepting more data. + @type localClosed: C{bool} + @ivar remoteClosed: True if the other size isn't accepting more data. + @type remoteClosed: C{bool} + """ + + implements(interfaces.ITransport) + name = None # only needed for client channels - def __init__(self, localWindow = 0, localMaxPacket = 0, - remoteWindow = 0, remoteMaxPacket = 0, + + def __init__(self, localWindow = 0, localMaxPacket = 0, + remoteWindow = 0, remoteMaxPacket = 0, conn = None, data=None, avatar = None): self.localWindowSize = localWindow or 131072 self.localWindowLeft = self.localWindowSize @@ -35,11 +73,13 @@ self.id = None # gets set later by SSHConnection def __str__(self): - return '%s (lw %i rw %i)' % (self.name, self.localWindowLeft, self.remoteWindowLeft) + return '' % (self.name, + self.localWindowLeft, self.remoteWindowLeft) def logPrefix(self): id = (self.id is not None and str(self.id)) or "unknown" - return "SSHChannel %s (%s) on %s" % (self.name, id, self.conn.logPrefix()) + return "SSHChannel %s (%s) on %s" % (self.name, id, + self.conn.logPrefix()) def channelOpen(self, specificData): """ @@ -68,7 +108,7 @@ """ self.remoteWindowLeft = self.remoteWindowLeft+bytes if not self.areWriting and not self.closing: - self.areWriting = 0 + self.areWriting = True self.startWriting() if self.buf: b = self.buf @@ -77,8 +117,8 @@ if self.extBuf: b = self.extBuf self.extBuf = [] - for i in b: - self.writeExtended(*i) + for (type, data) in b: + self.writeExtended(type, data) def requestReceived(self, requestType, data): """ @@ -139,17 +179,18 @@ def write(self, data): """ Write some data to the channel. If there is not enough remote window - available, buffer until it is. + available, buffer until it is. Otherwise, split the data into + packets of length remoteMaxPacket and send them. @type data: C{str} """ - #if not data: return if self.buf: self.buf += data return top = len(data) if top > self.remoteWindowLeft: - data, self.buf = data[:self.remoteWindowLeft], data[self.remoteWindowLeft:] + data, self.buf = (data[:self.remoteWindowLeft], + data[self.remoteWindowLeft:]) self.areWriting = 0 self.stopWriting() top = self.remoteWindowLeft @@ -158,38 +199,38 @@ r = range(0, top, rmp) for offset in r: write(self, data[offset: offset+rmp]) - self.remoteWindowLeft-=top + self.remoteWindowLeft -= top if self.closing and not self.buf: self.loseConnection() # try again def writeExtended(self, dataType, data): """ Send extended data to this channel. If there is not enough remote - window available, buffer until there is. + window available, buffer until there is. Otherwise, split the data + into packets of length remoteMaxPacket and send them. @type dataType: C{int} @type data: C{str} """ if self.extBuf: if self.extBuf[-1][0] == dataType: - self.extBuf[-1][1]+=data + self.extBuf[-1][1] += data else: self.extBuf.append([dataType, data]) return if len(data) > self.remoteWindowLeft: - data, self.extBuf = data[:self.remoteWindowLeft], \ - [[dataType, data[self.remoteWindowLeft:]]] + data, self.extBuf = (data[:self.remoteWindowLeft], + [[dataType, data[self.remoteWindowLeft:]]]) self.areWriting = 0 self.stopWriting() - if not data: return while len(data) > self.remoteMaxPacket: - self.conn.sendExtendedData(self, dataType, + self.conn.sendExtendedData(self, dataType, data[:self.remoteMaxPacket]) data = data[self.remoteMaxPacket:] - self.remoteWindowLeft-=self.remoteMaxPacket + self.remoteWindowLeft -= self.remoteMaxPacket if data: self.conn.sendExtendedData(self, dataType, data) - self.remoteWindowLeft-=len(data) + self.remoteWindowLeft -= len(data) if self.closing: self.loseConnection() # try again @@ -204,7 +245,8 @@ def loseConnection(self): """ - Close the channel. + Close the channel if there is no buferred data. Otherwise, note the + request and return. """ self.closing = 1 if not self.buf and not self.extBuf: diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/common.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/common.py --- twisted-conch-0.8.0/twisted/conch/ssh/common.py 2004-11-23 00:37:45.000000000 +0000 +++ twisted-conch-8.2.0/twisted/conch/ssh/common.py 2008-07-29 21:13:54.000000000 +0100 @@ -1,26 +1,38 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# -*- test-case-name: twisted.conch.test.test_ssh -*- +# Copyright (c) 2001-2007 Twisted Matrix Laboratories. # See LICENSE for details. -# -"""Common functions for the SSH classes. - -This module is unstable. +""" +Common functions for the SSH classes. -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ -import struct +import struct, warnings + try: from Crypto import Util - from Crypto.Util import randpool except ImportError: - import warnings - warnings.warn("PyCrypto not installed, but continuing anyways!", + warnings.warn("PyCrypto not installed, but continuing anyways!", RuntimeWarning) -else: - entropy = randpool.RandomPool() - entropy.stir() + +from twisted.python import randbytes + +class Entropy(object): + """ + A Crypto.Util.randpool.RandomPool mock for compatibility. + """ + def get_bytes(self, numBytes): + """ + Get a number of random bytes. + """ + warnings.warn("entropy.get_bytes is deprecated, please use " + "twisted.python.randbytes.secureRandom instead.", + category=DeprecationWarning, stacklevel=2) + return randbytes.secureRandom(numBytes) + +entropy = Entropy() def NS(t): @@ -49,12 +61,21 @@ bn = '\000' + bn return struct.pack('>L',len(bn)) + bn -def getMP(data): +def getMP(data, count=1): """ - get multiple precision integer + Get multiple precision integer out of the string. A multiple precision + integer is stored as a 4-byte length followed by length bytes of the + integer. If count is specified, get count integers out of the string. + The return value is a tuple of count integers followed by the rest of + the data. """ - length=struct.unpack('>L',data[:4])[0] - return Util.number.bytes_to_long(data[4:4+length]),data[4+length:] + mp = [] + c = 0 + for i in range(count): + length, = struct.unpack('>L',data[c:c+4]) + mp.append(Util.number.bytes_to_long(data[c+4:c+4+length])) + c += 4 + length + return tuple(mp) + (data[c:],) def _MPpow(x, y, z): """return the MP version of (x**y)%z @@ -74,10 +95,14 @@ _MPpow_py = _MPpow pyPow = pow -def _fastgetMP(i): - l = struct.unpack('!L', i[:4])[0] - n = i[4:l+4][::-1] - return long(gmpy.mpz(n+'\x00', 256)), i[4+l:] +def _fastgetMP(data, count=1): + mp = [] + c = 0 + for i in range(count): + length = struct.unpack('!L', data[c:c+4])[0] + mp.append(long(gmpy.mpz(data[c + 4:c + 4 + length][::-1] + '\x00', 256))) + c += length + 4 + return tuple(mp) + (data[c:],) def _fastMP(i): i2 = gmpy.mpz(i).binary()[::-1] @@ -96,10 +121,10 @@ MP = _fastMP _MPpow = _fastMPpow __builtins__['pow'] = _fastpow # evil evil - + try: import gmpy install() except ImportError: pass - + diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/connection.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/connection.py --- twisted-conch-0.8.0/twisted/conch/ssh/connection.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ssh/connection.py 2008-07-29 21:13:54.000000000 +0100 @@ -1,24 +1,43 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# -*- test-case-name: twisted.conch.test.test_connection -*- +# Copyright (c) 2001-2007 Twisted Matrix Laboratories. # See LICENSE for details. # -"""This module contains the implementation of the ssh-connection service, which +""" +This module contains the implementation of the ssh-connection service, which allows access to the shell and port-forwarding. -This module is unstable. - -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ -import struct, types +import struct -from twisted.internet import protocol, reactor, defer -from twisted.python import log +from twisted.conch.ssh import service, common from twisted.conch import error -import service, common, session, forwarding +from twisted.internet import defer +from twisted.python import log class SSHConnection(service.SSHService): + """ + An implementation of the 'ssh-connection' service. It is used to + multiplex multiple channels over the single SSH connection. + + @ivar localChannelID: the next number to use as a local channel ID. + @type localChannelID: C{int} + @ivar channels: a C{dict} mapping a local channel ID to C{SSHChannel} + subclasses. + @type channels: C{dict} + @ivar localToRemoteChannel: a C{dict} mapping a local channel ID to a + remote channel ID. + @type localToRemoteChannel: C{dict} + @ivar channelsToRemoteChannel: a C{dict} mapping a C{SSHChannel} subclass + to remote channel ID. + @type channelsToRemoteChannel: C{dict} + @ivar deferreds: a C{dict} mapping a local channel ID to a C{list} of + C{Deferreds} for outstanding channel requests. Also, the 'global' + key stores the C{list} of pending global request C{Deferred}s. + """ name = 'ssh-connection' def __init__(self): @@ -33,69 +52,107 @@ self.transport = None # gets set later def serviceStarted(self): - if hasattr(self.transport, 'avatar'): + if hasattr(self.transport, 'avatar'): self.transport.avatar.conn = self - + def serviceStopped(self): map(self.channelClosed, self.channels.values()) # packet methods def ssh_GLOBAL_REQUEST(self, packet): + """ + The other side has made a global request. Payload:: + string request type + bool want reply + + + This dispatches to self.gotGlobalRequest. + """ requestType, rest = common.getNS(packet) wantReply, rest = ord(rest[0]), rest[1:] - reply = MSG_REQUEST_FAILURE - data = '' ret = self.gotGlobalRequest(requestType, rest) - if ret: - reply = MSG_REQUEST_SUCCESS - if type(ret) in (types.TupleType, types.ListType): - data = ret[1] - else: - reply = MSG_REQUEST_FAILURE if wantReply: + reply = MSG_REQUEST_FAILURE + data = '' + if ret: + reply = MSG_REQUEST_SUCCESS + if isinstance(ret, (tuple, list)): + data = ret[1] self.transport.sendPacket(reply, data) def ssh_REQUEST_SUCCESS(self, packet): - data = packet + """ + Our global request succeeded. Get the appropriate Deferred and call + it back with the packet we received. + """ log.msg('RS') - self.deferreds['global'].pop(0).callback(data) + self.deferreds['global'].pop(0).callback(packet) def ssh_REQUEST_FAILURE(self, packet): + """ + Our global request failed. Get the appropriate Deferred and errback + it with the packet we received. + """ log.msg('RF') self.deferreds['global'].pop(0).errback( error.ConchError('global request failed', packet)) def ssh_CHANNEL_OPEN(self, packet): + """ + The other side wants to get a channel. Payload:: + string channel name + uint32 remote channel number + uint32 remote window size + uint32 remote maximum packet size + + + We get a channel from self.getChannel(), give it a local channel number + and notify the other side. Then notify the channel by calling its + channelOpen method. + """ channelType, rest = common.getNS(packet) - senderChannel, windowSize, maxPacket = struct.unpack('>3L', rest[: 12]) + senderChannel, windowSize, maxPacket = struct.unpack('>3L', rest[:12]) packet = rest[12:] try: - channel = self.getChannel(channelType, windowSize, maxPacket, packet) + channel = self.getChannel(channelType, windowSize, maxPacket, + packet) localChannel = self.localChannelID - self.localChannelID+=1 + self.localChannelID += 1 channel.id = localChannel self.channels[localChannel] = channel self.channelsToRemoteChannel[channel] = senderChannel self.localToRemoteChannel[localChannel] = senderChannel - self.transport.sendPacket(MSG_CHANNEL_OPEN_CONFIRMATION, - struct.pack('>4L', senderChannel, localChannel, - channel.localWindowSize, + self.transport.sendPacket(MSG_CHANNEL_OPEN_CONFIRMATION, + struct.pack('>4L', senderChannel, localChannel, + channel.localWindowSize, channel.localMaxPacket)+channel.specificData) - log.callWithLogger(channel, channel.channelOpen, '') + log.callWithLogger(channel, channel.channelOpen, packet) except Exception, e: log.msg('channel open failed') log.err(e) if isinstance(e, error.ConchError): - reason, textualInfo = e.args[0], e.data + textualInfo, reason = e.args else: reason = OPEN_CONNECT_FAILED textualInfo = "unknown failure" - self.transport.sendPacket(MSG_CHANNEL_OPEN_FAILURE, - struct.pack('>2L', senderChannel, reason)+ \ - common.NS(textualInfo)+common.NS('')) + self.transport.sendPacket(MSG_CHANNEL_OPEN_FAILURE, + struct.pack('>2L', senderChannel, reason) + + common.NS(textualInfo) + common.NS('')) def ssh_CHANNEL_OPEN_CONFIRMATION(self, packet): - localChannel, remoteChannel, windowSize, maxPacket = struct.unpack('>4L', packet[: 16]) + """ + The other side accepted our MSG_CHANNEL_OPEN request. Payload:: + uint32 local channel number + uint32 remote channel number + uint32 remote window size + uint32 remote maximum packet size + + + Find the channel using the local channel number and notify its + channelOpen method. + """ + (localChannel, remoteChannel, windowSize, + maxPacket) = struct.unpack('>4L', packet[: 16]) specificData = packet[16:] channel = self.channels[localChannel] channel.conn = self @@ -106,7 +163,16 @@ log.callWithLogger(channel, channel.channelOpen, specificData) def ssh_CHANNEL_OPEN_FAILURE(self, packet): - localChannel, reasonCode = struct.unpack('>2L', packet[: 8]) + """ + The other side did not accept our MSG_CHANNEL_OPEN request. Payload:: + uint32 local channel number + uint32 reason code + string reason description + + Find the channel using the local channel number and notify it by + calling its openFailed() method. + """ + localChannel, reasonCode = struct.unpack('>2L', packet[:8]) reasonDesc = common.getNS(packet[8:])[0] channel = self.channels[localChannel] del self.channels[localChannel] @@ -115,23 +181,41 @@ log.callWithLogger(channel, channel.openFailed, reason) def ssh_CHANNEL_WINDOW_ADJUST(self, packet): - localChannel, bytesToAdd = struct.unpack('>2L', packet[: 8]) + """ + The other side is adding bytes to its window. Payload:: + uint32 local channel number + uint32 bytes to add + + Call the channel's addWindowBytes() method to add new bytes to the + remote window. + """ + localChannel, bytesToAdd = struct.unpack('>2L', packet[:8]) channel = self.channels[localChannel] log.callWithLogger(channel, channel.addWindowBytes, bytesToAdd) def ssh_CHANNEL_DATA(self, packet): - localChannel, dataLength = struct.unpack('>2L', packet[: 8]) + """ + The other side is sending us data. Payload:: + uint32 local channel number + string data + + Check to make sure the other side hasn't sent too much data (more + than what's in the window, or more than the maximum packet size). If + they have, close the channel. Otherwise, decrease the available + window and pass the data to the channel's dataReceived(). + """ + localChannel, dataLength = struct.unpack('>2L', packet[:8]) channel = self.channels[localChannel] # XXX should this move to dataReceived to put client in charge? - if dataLength > channel.localWindowLeft or \ - dataLength > channel.localMaxPacket: # more data than we want - log.callWithLogger(channel, lambda s=self,c=channel: - log.msg('too much data') and s.sendClose(c)) + if (dataLength > channel.localWindowLeft or + dataLength > channel.localMaxPacket): # more data than we want + log.callWithLogger(channel, log.msg, 'too much data') + self.sendClose(channel) return #packet = packet[:channel.localWindowLeft+4] data = common.getNS(packet[4:])[0] - channel.localWindowLeft-=dataLength - if channel.localWindowLeft < channel.localWindowSize/2: + channel.localWindowLeft -= dataLength + if channel.localWindowLeft < channel.localWindowSize / 2: self.adjustWindow(channel, channel.localWindowSize - \ channel.localWindowLeft) #log.msg('local window left: %s/%s' % (channel.localWindowLeft, @@ -139,76 +223,145 @@ log.callWithLogger(channel, channel.dataReceived, data) def ssh_CHANNEL_EXTENDED_DATA(self, packet): - localChannel, typeCode, dataLength = struct.unpack('>3L', packet[: 12]) + """ + The other side is sending us exteneded data. Payload:: + uint32 local channel number + uint32 type code + string data + + Check to make sure the other side hasn't sent too much data (more + than what's in the window, or or than the maximum packet size). If + they have, close the channel. Otherwise, decrease the available + window and pass the data and type code to the channel's + extReceived(). + """ + localChannel, typeCode, dataLength = struct.unpack('>3L', packet[:12]) channel = self.channels[localChannel] - if dataLength > channel.localWindowLeft or \ - dataLength > channel.localMaxPacket: - log.callWithLogger(channel, lambda s=self,c=channel: - log.msg('too much extdata') and s.sendClose(c)) + if (dataLength > channel.localWindowLeft or + dataLength > channel.localMaxPacket): + log.callWithLogger(channel, log.msg, 'too much extdata') + self.sendClose(channel) return data = common.getNS(packet[8:])[0] channel.localWindowLeft -= dataLength - if channel.localWindowLeft < channel.localWindowSize/2: - self.adjustWindow(channel, channel.localWindowSize - \ + if channel.localWindowLeft < channel.localWindowSize / 2: + self.adjustWindow(channel, channel.localWindowSize - channel.localWindowLeft) log.callWithLogger(channel, channel.extReceived, typeCode, data) def ssh_CHANNEL_EOF(self, packet): - localChannel = struct.unpack('>L', packet[: 4])[0] + """ + The other side is not sending any more data. Payload:: + uint32 local channel number + + Notify the channel by calling its eofReceived() method. + """ + localChannel = struct.unpack('>L', packet[:4])[0] channel = self.channels[localChannel] log.callWithLogger(channel, channel.eofReceived) def ssh_CHANNEL_CLOSE(self, packet): - localChannel = struct.unpack('>L', packet[: 4])[0] + """ + The other side is closing its end; it does not want to receive any + more data. Payload:: + uint32 local channel number + + Notify the channnel by calling its closeReceived() method. If + the channel has also sent a close message, call self.channelClosed(). + """ + localChannel = struct.unpack('>L', packet[:4])[0] channel = self.channels[localChannel] - if channel.remoteClosed: - return log.callWithLogger(channel, channel.closeReceived) - channel.remoteClosed = 1 + channel.remoteClosed = True if channel.localClosed and channel.remoteClosed: self.channelClosed(channel) def ssh_CHANNEL_REQUEST(self, packet): + """ + The other side is sending a request to a channel. Payload:: + uint32 local channel number + string request name + bool want reply + + + Pass the message to the channel's requestReceived method. If the + other side wants a reply, add callbacks which will send the + reply. + """ localChannel = struct.unpack('>L', packet[: 4])[0] requestType, rest = common.getNS(packet[4:]) wantReply = ord(rest[0]) channel = self.channels[localChannel] - d = log.callWithLogger(channel, channel.requestReceived, requestType, rest[1:]) + d = defer.maybeDeferred(log.callWithLogger, channel, + channel.requestReceived, requestType, rest[1:]) if wantReply: - if isinstance(d, defer.Deferred): - d.addCallback(self._cbChannelRequest, localChannel) - d.addErrback(self._ebChannelRequest, localChannel) - elif d: - self._cbChannelRequest(None, localChannel) - else: - self._ebChannelRequest(None, localChannel) + d.addCallback(self._cbChannelRequest, localChannel) + d.addErrback(self._ebChannelRequest, localChannel) + return d def _cbChannelRequest(self, result, localChannel): - self.transport.sendPacket(MSG_CHANNEL_SUCCESS, struct.pack('>L', - self.localToRemoteChannel[localChannel])) + """ + Called back if the other side wanted a reply to a channel request. If + the result is true, send a MSG_CHANNEL_SUCCESS. Otherwise, raise + a C{error.ConchError} + + @param result: the value returned from the channel's requestReceived() + method. If it's False, the request failed. + @type result: C{bool} + @param localChannel: the local channel ID of the channel to which the + request was made. + @type localChannel: C{int} + @raises ConchError: if the result is False. + """ + if not result: + raise error.ConchError('failed request') + self.transport.sendPacket(MSG_CHANNEL_SUCCESS, struct.pack('>L', + self.localToRemoteChannel[localChannel])) def _ebChannelRequest(self, result, localChannel): - self.transport.sendPacket(MSG_CHANNEL_FAILURE, struct.pack('>L', - self.localToRemoteChannel[localChannel])) + """ + Called if the other wisde wanted a reply to the channel requeset and + the channel request failed. + + @param result: a Failure, but it's not used. + @param localChannel: the local channel ID of the channel to which the + request was made. + @type localChannel: C{int} + """ + self.transport.sendPacket(MSG_CHANNEL_FAILURE, struct.pack('>L', + self.localToRemoteChannel[localChannel])) def ssh_CHANNEL_SUCCESS(self, packet): - localChannel = struct.unpack('>L', packet[: 4])[0] + """ + Our channel request to the other other side succeeded. Payload:: + uint32 local channel number + + Get the C{Deferred} out of self.deferreds and call it back. + """ + localChannel = struct.unpack('>L', packet[:4])[0] if self.deferreds.get(localChannel): d = self.deferreds[localChannel].pop(0) - log.callWithLogger(self.channels[localChannel], - d.callback, packet[4:]) + log.callWithLogger(self.channels[localChannel], + d.callback, '') def ssh_CHANNEL_FAILURE(self, packet): - localChannel = struct.unpack('>L', packet[: 4])[0] + """ + Our channel request to the other side failed. Payload:: + uint32 local channel number + + Get the C{Deferred} out of self.deferreds and errback it with a + C{error.ConchError}. + """ + localChannel = struct.unpack('>L', packet[:4])[0] if self.deferreds.get(localChannel): d = self.deferreds[localChannel].pop(0) log.callWithLogger(self.channels[localChannel], - d.errback, + d.errback, error.ConchError('channel request failed')) # methods for users of the connection to call - def sendGlobalRequest(self, request, data, wantReply = 0): + def sendGlobalRequest(self, request, data, wantReply=0): """ Send a global request for this connection. Current this is only used for remote->local TCP forwarding. @@ -227,24 +380,24 @@ self.deferreds.setdefault('global', []).append(d) return d - def openChannel(self, channel, extra = ''): + def openChannel(self, channel, extra=''): """ Open a new channel on this connection. @type channel: subclass of C{SSHChannel} @type extra: C{str} """ - log.msg('opening channel %s with %s %s'%(self.localChannelID, + log.msg('opening channel %s with %s %s'%(self.localChannelID, channel.localWindowSize, channel.localMaxPacket)) self.transport.sendPacket(MSG_CHANNEL_OPEN, common.NS(channel.name) - +struct.pack('>3L', self.localChannelID, + + struct.pack('>3L', self.localChannelID, channel.localWindowSize, channel.localMaxPacket) - +extra) + + extra) channel.id = self.localChannelID self.channels[self.localChannelID] = channel - self.localChannelID+=1 + self.localChannelID += 1 - def sendRequest(self, channel, requestType, data, wantReply = 0): + def sendRequest(self, channel, requestType, data, wantReply=0): """ Send a request to a channel. @@ -257,7 +410,7 @@ if channel.localClosed: return log.msg('sending request %s' % requestType) - self.transport.sendPacket(MSG_CHANNEL_REQUEST, struct.pack('>L', + self.transport.sendPacket(MSG_CHANNEL_REQUEST, struct.pack('>L', self.channelsToRemoteChannel[channel]) + common.NS(requestType)+chr(wantReply) + data) @@ -276,11 +429,12 @@ """ if channel.localClosed: return # we're already closed - self.transport.sendPacket(MSG_CHANNEL_WINDOW_ADJUST, struct.pack('>2L', - self.channelsToRemoteChannel[channel], + self.transport.sendPacket(MSG_CHANNEL_WINDOW_ADJUST, struct.pack('>2L', + self.channelsToRemoteChannel[channel], bytesToAdd)) - log.msg('adding %i to %i in channel %i' % (bytesToAdd, channel.localWindowLeft, channel.id)) - channel.localWindowLeft+=bytesToAdd + log.msg('adding %i to %i in channel %i' % (bytesToAdd, + channel.localWindowLeft, channel.id)) + channel.localWindowLeft += bytesToAdd def sendData(self, channel, data): """ @@ -292,8 +446,8 @@ """ if channel.localClosed: return # we're already closed - self.transport.sendPacket(MSG_CHANNEL_DATA, struct.pack('>L', - self.channelsToRemoteChannel[channel])+ \ + self.transport.sendPacket(MSG_CHANNEL_DATA, struct.pack('>L', + self.channelsToRemoteChannel[channel]) + common.NS(data)) def sendExtendedData(self, channel, dataType, data): @@ -308,7 +462,7 @@ """ if channel.localClosed: return # we're already closed - self.transport.sendPacket(MSG_CHANNEL_EXTENDED_DATA, struct.pack('>2L', + self.transport.sendPacket(MSG_CHANNEL_EXTENDED_DATA, struct.pack('>2L', self.channelsToRemoteChannel[channel],dataType) \ + common.NS(data)) @@ -321,7 +475,7 @@ if channel.localClosed: return # we're already closed log.msg('sending eof') - self.transport.sendPacket(MSG_CHANNEL_EOF, struct.pack('>L', + self.transport.sendPacket(MSG_CHANNEL_EOF, struct.pack('>L', self.channelsToRemoteChannel[channel])) def sendClose(self, channel): @@ -333,9 +487,9 @@ if channel.localClosed: return # we're already closed log.msg('sending close %i' % channel.id) - self.transport.sendPacket(MSG_CHANNEL_CLOSE, struct.pack('>L', - self.channelsToRemoteChannel[channel])) - channel.localClosed = 1 + self.transport.sendPacket(MSG_CHANNEL_CLOSE, struct.pack('>L', + self.channelsToRemoteChannel[channel])) + channel.localClosed = True if channel.localClosed and channel.remoteClosed: self.channelClosed(channel) @@ -351,7 +505,7 @@ We return a subclass of L{SSHChannel}. By default, this dispatches to a method 'channel_channelType' with any - non-alphanumerics in the channelType replace with _'s. If it cannot + non-alphanumerics in the channelType replace with _'s. If it cannot find a suitable method, it returns an OPEN_UNKNOWN_CHANNEL_TYPE error. The method is called with arguments of windowSize, maxPacket, data. @@ -363,18 +517,23 @@ """ log.msg('got channel %s request' % channelType) if hasattr(self.transport, "avatar"): # this is a server! - chan = self.transport.avatar.lookupChannel(channelType, - windowSize, - maxPacket, + chan = self.transport.avatar.lookupChannel(channelType, + windowSize, + maxPacket, data) - chan.conn = self - return chan else: channelType = channelType.translate(TRANSLATE_TABLE) f = getattr(self, 'channel_%s' % channelType, None) - if not f: - return OPEN_UNKNOWN_CHANNEL_TYPE, "don't know that channel" - return f(windowSize, maxPacket, data) + if f is not None: + chan = f(windowSize, maxPacket, data) + else: + chan = None + if chan is None: + raise error.ConchError('unknown channel', + OPEN_UNKNOWN_CHANNEL_TYPE) + else: + chan.conn = self + return chan def gotGlobalRequest(self, requestType, data): """ @@ -387,7 +546,7 @@ By default, this dispatches to a method 'global_requestType' with -'s in requestType replaced with _'s. The found method is passed data. - If this method cannot be found, this method returns 0. Otherwise, it + If this method cannot be found, this method returns 0. Otherwise, it returns the return value of that method. @type requestType: C{str} @@ -407,17 +566,18 @@ def channelClosed(self, channel): """ Called when a channel is closed. - It clears the local state related to the channel, and calls + It clears the local state related to the channel, and calls channel.closed(). MAKE SURE YOU CALL THIS METHOD, even if you subclass L{SSHConnection}. If you don't, things will break mysteriously. """ - channel.localClosed = channel.remoteClosed = 1 - del self.localToRemoteChannel[channel.id] - del self.channels[channel.id] - del self.channelsToRemoteChannel[channel] - self.deferreds[channel.id] = [] - log.callWithLogger(channel, channel.closed) + if channel in self.channelsToRemoteChannel: # actually open + channel.localClosed = channel.remoteClosed = True + del self.localToRemoteChannel[channel.id] + del self.channels[channel.id] + del self.channelsToRemoteChannel[channel] + self.deferreds[channel.id] = [] + log.callWithLogger(channel, channel.closed) MSG_GLOBAL_REQUEST = 80 MSG_REQUEST_SUCCESS = 81 @@ -442,12 +602,12 @@ EXTENDED_DATA_STDERR = 1 messages = {} -import connection -for v in dir(connection): - if v[: 4] == 'MSG_': - messages[getattr(connection, v)] = v # doesn't handle doubles +for name, value in locals().copy().items(): + if name[:4] == 'MSG_': + messages[value] = name # doesn't handle doubles import string alphanums = string.letters + string.digits -TRANSLATE_TABLE = ''.join([chr(i) in alphanums and chr(i) or '_' for i in range(256)]) +TRANSLATE_TABLE = ''.join([chr(i) in alphanums and chr(i) or '_' + for i in range(256)]) SSHConnection.protocolMessages = messages diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/factory.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/factory.py --- twisted-conch-0.8.0/twisted/conch/ssh/factory.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ssh/factory.py 2008-09-20 04:15:34.000000000 +0100 @@ -1,16 +1,12 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE for details. -# - -"""A Factory for SSH servers, along with an OpenSSHFactory to use the same data sources as OpenSSH. - -This module is unstable. - -Maintainer: U{Paul Swartz} """ +A Factory for SSH servers, along with an OpenSSHFactory to use the same +data sources as OpenSSH. -import md5 +Maintainer: Paul Swartz +""" try: import resource @@ -19,14 +15,19 @@ from twisted.internet import protocol from twisted.python import log +from twisted.python.reflect import qual from twisted.conch import error - +from twisted.conch.ssh import keys import transport, userauth, connection import random +import warnings class SSHFactory(protocol.Factory): + + protocol = transport.SSHServerTransport + services = { 'ssh-userauth':userauth.SSHUserAuthServer, 'ssh-connection':connection.SSHConnection @@ -39,47 +40,62 @@ log.msg('INSECURE: unable to disable core dumps.') if not hasattr(self,'publicKeys'): self.publicKeys = self.getPublicKeys() + for keyType, value in self.publicKeys.items(): + if isinstance(value, str): + warnings.warn("Returning a mapping from strings to " + "strings from getPublicKeys()/publicKeys (in %s) " + "is deprecated. Return a mapping from " + "strings to Key objects instead." % + (qual(self.__class__)), + DeprecationWarning, stacklevel=1) + self.publicKeys[keyType] = keys.Key.fromString(value) if not hasattr(self,'privateKeys'): self.privateKeys = self.getPrivateKeys() + for keyType, value in self.privateKeys.items(): + if not isinstance(value, keys.Key): + warnings.warn("Returning a mapping from strings to " + "PyCrypto key objects from " + "getPrivateKeys()/privateKeys (in %s) " + "is deprecated. Return a mapping from " + "strings to Key objects instead." % + (qual(self.__class__),), + DeprecationWarning, stacklevel=1) + self.privateKeys[keyType] = keys.Key(value) if not self.publicKeys or not self.privateKeys: raise error.ConchError('no host keys, failing') if not hasattr(self,'primes'): self.primes = self.getPrimes() - #if not self.primes: - # log.msg('disabling diffie-hellman-group-exchange because we cannot find moduli file') - # transport.SSHServerTransport.supportedKeyExchanges.remove('diffie-hellman-group-exchange-sha1') - if self.primes: - self.primesKeys = self.primes.keys() def buildProtocol(self, addr): - t = transport.SSHServerTransport() + t = protocol.Factory.buildProtocol(self, addr) t.supportedPublicKeys = self.privateKeys.keys() if not self.primes: + log.msg('disabling diffie-hellman-group-exchange because we ' + 'cannot find moduli file') ske = t.supportedKeyExchanges[:] ske.remove('diffie-hellman-group-exchange-sha1') t.supportedKeyExchanges = ske - t.factory = self return t def getPublicKeys(self): """ - Called when the factory is started to get the public portions of the - servers host keys. Returns a dictionary mapping SSH key types to + Called when the factory is started to get the public portions of the + servers host keys. Returns a dictionary mapping SSH key types to public key strings. @rtype: C{dict} """ - raise NotImplementedError + raise NotImplementedError('getPublicKeys unimplemented') def getPrivateKeys(self): """ - Called when the factory is started to get the private portions of the - servers host keys. Returns a dictionary mapping SSH key types to + Called when the factory is started to get the private portions of the + servers host keys. Returns a dictionary mapping SSH key types to C{Crypto.PublicKey.pubkey.pubkey} objects. @rtype: C{dict} """ - raise NotImplementedError + raise NotImplementedError('getPrivateKeys unimplemented') def getPrimes(self): """ @@ -98,8 +114,9 @@ @type bits: C{int} @rtype: C{tuple} """ - self.primesKeys.sort(lambda x,y,b=bits:cmp(abs(x-b), abs(x-b))) - realBits = self.primesKeys[0] + primesKeys = self.primes.keys() + primesKeys.sort(lambda x, y: cmp(abs(x - bits), abs(y - bits))) + realBits = primesKeys[0] return random.choice(self.primes[realBits]) def getService(self, transport, service): @@ -107,10 +124,8 @@ Return a class to use as a service for the given transport. @type transport: L{transport.SSHServerTransport} - @type service: C{stR} + @type service: C{str} @rtype: subclass of L{service.SSHService} """ - if transport.isAuthorized or service == 'ssh-userauth': + if service == 'ssh-userauth' or hasattr(transport, 'avatar'): return self.services[service] - - diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/filetransfer.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/filetransfer.py --- twisted-conch-0.8.0/twisted/conch/ssh/filetransfer.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ssh/filetransfer.py 2008-11-01 15:32:22.000000000 +0000 @@ -1,9 +1,8 @@ # -*- test-case-name: twisted.conch.test.test_filetransfer -*- # -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE for details. -# import struct, errno @@ -15,6 +14,8 @@ from zope import interface + + class FileTransferBase(protocol.Protocol): versions = (3, ) @@ -79,7 +80,7 @@ attrs['mtime'] = mtime data = data[8:] if flags & FILEXFER_ATTR_EXTENDED == FILEXFER_ATTR_EXTENDED: - extended_count ,= struct.unpack('!L', data[4:]) + extended_count ,= struct.unpack('!L', data[:4]) data = data[4:] for i in xrange(extended_count): extended_type, data = getNS(data) @@ -309,7 +310,7 @@ data += NS(filename) data += NS(longname) data += self._packAttributes(attrs) - self.sendPacket(FXP_NAME, requestId + + self.sendPacket(FXP_NAME, requestId + struct.pack('!L', len(result))+data) def packet_STAT(self, data, followLinks = 1): @@ -330,7 +331,7 @@ handle, data = getNS(data) assert data == '', 'still have data in FSTAT: %s' % repr(data) if handle not in self.openFiles: - self._ebStatus(failure.Failure(KeyError('%s not in self.openFiles' + self._ebStatus(failure.Failure(KeyError('%s not in self.openFiles' % handle)), requestId) else: fileObj = self.openFiles[handle] @@ -421,6 +422,8 @@ elif reason.value.errno == errno.EACCES: # permission denied code = FX_PERMISSION_DENIED message = reason.value.strerror + elif reason.value.errno == errno.EEXIST: + code = FX_FILE_ALREADY_EXISTS else: log.err(reason) elif reason.type == EOFError: # EOF @@ -447,6 +450,20 @@ data += NS(lang) self.sendPacket(FXP_STATUS, data) + + def connectionLost(self, reason): + """ + Clean all opened files and directories. + """ + for fileObj in self.openFiles.values(): + fileObj.close() + self.openFiles = {} + for (dirObj, dirIter) in self.openDirs.values(): + dirObj.close() + self.openDirs = {} + + + class FileTransferClient(FileTransferBase): def __init__(self, extData = {}): @@ -538,7 +555,7 @@ """ Make a directory. - This method returns a Deferred that is called back when it is + This method returns a Deferred that is called back when it is created. @param path: the name of the directory to create as a string. @@ -565,7 +582,7 @@ """ Open a directory for scanning. - This method returns a Deferred that is called back with an iterable + This method returns a Deferred that is called back with an iterable object that has a close() method. The close() method is called when the client is finished reading @@ -573,7 +590,7 @@ be used. The iterable returns triples of the form (filename, longname, attrs) - or a Deferred that returns the same. The sequence must support + or a Deferred that returns the same. The sequence must support __getitem__, but otherwise may be any 'sequence-like' object. filename is the name of the file relative to the directory. @@ -849,10 +866,11 @@ FXP_EXTENDED_REPLY = 201 FILEXFER_ATTR_SIZE = 0x00000001 -FILEXFER_ATTR_OWNERGROUP = 0x00000002 +FILEXFER_ATTR_UIDGID = 0x00000002 +FILEXFER_ATTR_OWNERGROUP = FILEXFER_ATTR_UIDGID FILEXFER_ATTR_PERMISSIONS = 0x00000004 -FILEXFER_ATTR_ACMODTIME = 0x00000009 -FILEXFER_ATTR_EXTENDED = 0x+80000000 +FILEXFER_ATTR_ACMODTIME = 0x00000008 +FILEXFER_ATTR_EXTENDED = 0x80000000L FILEXFER_TYPE_REGULAR = 1 FILEXFER_TYPE_DIRECTORY = 2 @@ -877,11 +895,11 @@ FX_NO_CONNECTION = 6 FX_CONNECTION_LOST = 7 FX_OP_UNSUPPORTED = 8 -# http://www.ietf.org/internet-drafts/draft-ietf-secsh-filexfer-12.txt defines -# more useful error codes, but so far OpenSSH doesn't implement them. We use -# them internally for clarity, but for now define them all as FX_FAILURE to be +FX_FILE_ALREADY_EXISTS = 11 +# http://tools.ietf.org/wg/secsh/draft-ietf-secsh-filexfer/ defines more +# useful error codes, but so far OpenSSH doesn't implement them. We use them +# internally for clarity, but for now define them all as FX_FAILURE to be # compatible with existing software. -FX_FILE_ALREADY_EXISTS = FX_FAILURE FX_NOT_A_DIRECTORY = FX_FAILURE FX_FILE_IS_A_DIRECTORY = FX_FAILURE diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/forwarding.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/forwarding.py --- twisted-conch-0.8.0/twisted/conch/ssh/forwarding.py 2004-12-06 07:27:17.000000000 +0000 +++ twisted-conch-8.2.0/twisted/conch/ssh/forwarding.py 2008-07-29 21:13:54.000000000 +0100 @@ -3,12 +3,11 @@ # -"""This module contains the implementation of the TCP forwarding, which allows +""" +This module contains the implementation of the TCP forwarding, which allows clients and servers to forward arbitrary TCP data across the connection. -This module is unstable. - -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ import struct diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/__init__.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/__init__.py --- twisted-conch-0.8.0/twisted/conch/ssh/__init__.py 2004-08-25 09:36:30.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ssh/__init__.py 2008-07-29 21:13:54.000000000 +0100 @@ -3,9 +3,8 @@ # -"""An SSHv2 implementation for Twisted. Part of the Twisted.Conch package. - -This module is unstable. +""" +An SSHv2 implementation for Twisted. Part of the Twisted.Conch package. -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/keys.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/keys.py --- twisted-conch-0.8.0/twisted/conch/ssh/keys.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ssh/keys.py 2008-11-16 02:54:52.000000000 +0000 @@ -1,362 +1,833 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# -*- test-case-name: twisted.conch.test.test_keys -*- +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE for details. -# - -"""Handling of RSA and DSA keys. - -This module is unstable. +""" +Handling of RSA and DSA keys. -Maintainer: U{Paul Swartz} +Maintainer: U{Paul Swartz} """ # base library imports import base64 -import string import sha, md5 +import warnings # external library imports from Crypto.Cipher import DES3 from Crypto.PublicKey import RSA, DSA from Crypto import Util -#twisted -from twisted.python import log +# twisted +from twisted.python import randbytes # sibling imports -import asn1, common, sexpy +from twisted.conch.ssh import asn1, common, sexpy class BadKeyError(Exception): """ - raised when a key isn't what we expected from it. + Raised when a key isn't what we expected from it. XXX: we really need to check for bad keys """ -def getPublicKeyString(filename = None, line = 0, data = ''): +class EncryptedKeyError(Exception): + """ + Raised when an encrypted key is presented to fromString/fromFile without + a password. """ - Return a public key string given a filename or data of a public key. - Currently handles OpenSSH and LSH keys. + +class Key(object): + """ + An object representing a key. A key can be either a public or + private key. A public key can verify a signature; a private key can + create or verify a signature. To generate a string that can be stored + on disk, use the toString method. If you have a private key, but want + the string representation of the public key, use Key.public().toString(). + + @ivar keyObject: The C{Crypto.PublicKey.pubkey.pubkey} object that + operations are performed with. + """ + + def fromFile(Class, filename, type=None, passphrase=None): + """ + Return a Key object corresponding to the data in filename. type + and passphrase function as they do in fromString. + """ + return Class.fromString(file(filename, 'rb').read(), type, passphrase) + fromFile = classmethod(fromFile) + + def fromString(Class, data, type=None, passphrase=None): + """ + Return a Key object corresponding to the string data. + type is optionally the type of string, matching a _fromString_* + method. Otherwise, the _guessStringType() classmethod will be used + to guess a type. If the key is encrypted, passphrase is used as + the decryption key. + + @type data: C{str} + @type type: C{None}/C{str} + @type passphrase: C{None}/C{str} + @rtype: C{Key} + """ + if type is None: + type = Class._guessStringType(data) + if type is None: + raise BadKeyError('cannot guess the type of %r' % data) + method = getattr(Class, '_fromString_%s' % type.upper(), None) + if method is None: + raise BadKeyError('no _fromString method for %s' % type) + if method.func_code.co_argcount == 2: # no passphrase + if passphrase: + raise BadKeyError('key not encrypted') + return method(data) + else: + return method(data, passphrase) + fromString = classmethod(fromString) + + def _fromString_BLOB(Class, blob): + """ + Return a public key object corresponding to this public key blob. + The format of a RSA public key blob is:: + string 'ssh-rsa' + integer e + integer n + + The format of a DSA public key blob is:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + + @type blob: C{str} + @return: a C{Crypto.PublicKey.pubkey.pubkey} object + @raises BadKeyError: if the key type (the first string) is unknown. + """ + keyType, rest = common.getNS(blob) + if keyType == 'ssh-rsa': + e, n, rest = common.getMP(rest, 2) + return Class(RSA.construct((n, e))) + elif keyType == 'ssh-dss': + p, q, g, y, rest = common.getMP(rest, 4) + return Class(DSA.construct((y, g, p, q))) + else: + raise BadKeyError('unknown blob type: %s' % keyType) + _fromString_BLOB = classmethod(_fromString_BLOB) + + def _fromString_PRIVATE_BLOB(Class, blob): + """ + Return a private key object corresponding to this private key blob. + The blob formats are as follows: + + RSA keys:: + string 'ssh-rsa' + integer n + integer e + integer d + integer u + integer p + integer q + + DSA keys:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + integer x + + @type blob: C{str} + @return: a C{Crypto.PublicKey.pubkey.pubkey} object + @raises BadKeyError: if the key type (the first string) is unknown. + """ + keyType, rest = common.getNS(blob) + + if keyType == 'ssh-rsa': + n, e, d, u, p, q, rest = common.getMP(rest, 6) + rsakey = Class(RSA.construct((n, e, d, p, q, u))) + return rsakey + elif keyType == 'ssh-dss': + p, q, g, y, x, rest = common.getMP(rest, 5) + dsakey = Class(DSA.construct((y, g, p, q, x))) + return dsakey + else: + raise BadKeyError('unknown blob type: %s' % keyType) + _fromString_PRIVATE_BLOB = classmethod(_fromString_PRIVATE_BLOB) + + def _fromString_PUBLIC_OPENSSH(Class, data): + """ + Return a public key object corresponding to this OpenSSH public key + string. The format of an OpenSSH public key string is:: + + + @type data: C{str} + @return: A {Crypto.PublicKey.pubkey.pubkey} object + @raises BadKeyError: if the blob type is unknown. + """ + blob = base64.decodestring(data.split()[1]) + return Class._fromString_BLOB(blob) + _fromString_PUBLIC_OPENSSH = classmethod(_fromString_PUBLIC_OPENSSH) + + def _fromString_PRIVATE_OPENSSH(Class, data, passphrase): + """ + Return a private key object corresponding to this OpenSSH private key + string. If the key is encrypted, passphrase MUST be provided. + Providing a passphrase for an unencrypted key is an error. + + The format of an OpenSSH private key string is:: + -----BEGIN PRIVATE KEY----- + [Proc-Type: 4,ENCRYPTED + DEK-Info: DES-EDE3-CBC,] + + ------END PRIVATE KEY------ + + The ASN.1 structure of a RSA key is:: + (0, n, e, d, p, q) + + The ASN.1 structure of a DSA key is:: + (0, p, q, g, y, x) + + @type data: C{str} + @type passphrase: C{str} + @return: a C{Crypto.PublicKey.pubkey.pubkey} object + @raises BadKeyError: if + * a passphrase is provided for an unencrypted key + * a passphrase is not provided for an encrypted key + * the ASN.1 encoding is incorrect + """ + lines = [x + '\n' for x in data.split('\n')] + kind = lines[0][11:14] + if lines[1].startswith('Proc-Type: 4,ENCRYPTED'): # encrypted key + ivdata = lines[2].split(',')[1][:-1] + iv = ''.join([chr(int(ivdata[i:i + 2], 16)) for i in range(0, + len(ivdata), 2)]) + if not passphrase: + raise EncryptedKeyError('encrypted key with no passphrase') + ba = md5.new(passphrase + iv).digest() + bb = md5.new(ba + passphrase + iv).digest() + decKey = (ba + bb)[:24] + b64Data = base64.decodestring(''.join(lines[3:-1])) + keyData = DES3.new(decKey, DES3.MODE_CBC, iv).decrypt(b64Data) + removeLen = ord(keyData[-1]) + keyData = keyData[:-removeLen] + else: + keyData = base64.decodestring(''.join(lines[1:-1])) + try: + decodedKey = asn1.parse(keyData) + except Exception, e: + raise BadKeyError, 'something wrong with decode' + if kind == 'RSA': + if len(decodedKey) == 2: # alternate RSA key + decodedKey = decodedKey[0] + n, e, d, p, q = decodedKey[1:6] + if p > q: # make p smaller than q + p, q = q, p + return Class(RSA.construct((n, e, d, p, q))) + elif kind == 'DSA': + p, q, g, y, x = decodedKey[1: 6] + return Class(DSA.construct((y, g, p, q, x))) + _fromString_PRIVATE_OPENSSH = classmethod(_fromString_PRIVATE_OPENSSH) + + def _fromString_PUBLIC_LSH(Class, data): + """ + Return a public key corresponding to this LSH public key string. + The LSH public key string format is:: + , ()+))> + + The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e. + The names for a DSA (key type 'dsa') key are: y, g, p, q. + + @type data: C{str} + @return: a C{Crypto.PublicKey.pubkey.pubkey} object + @raises BadKeyError: if the key type is unknown + """ + sexp = sexpy.parse(base64.decodestring(data[1:-1])) + assert sexp[0] == 'public-key' + kd = {} + for name, data in sexp[1][1:]: + kd[name] = common.getMP(common.NS(data))[0] + if sexp[1][0] == 'dsa': + return Class(DSA.construct((kd['y'], kd['g'], kd['p'], kd['q']))) + elif sexp[1][0] == 'rsa-pkcs1-sha1': + return Class(RSA.construct((kd['n'], kd['e']))) + else: + raise BadKeyError('unknown lsh key type %s' % sexp[1][0]) + _fromString_PUBLIC_LSH = classmethod(_fromString_PUBLIC_LSH) + + def _fromString_PRIVATE_LSH(Class, data): + """ + Return a private key corresponding to this LSH private key string. + The LSH private key string format is:: + , (, )+))> + + The names for a RSA (key type 'rsa-pkcs1-sha1') key are: n, e, d, p, q. + The names for a DSA (key type 'dsa') key are: y, g, p, q, x. + + @type data: C{str} + @return: a {Crypto.PublicKey.pubkey.pubkey} object + @raises BadKeyError: if the key type is unknown + """ + sexp = sexpy.parse(data) + assert sexp[0] == 'private-key' + kd = {} + for name, data in sexp[1][1:]: + kd[name] = common.getMP(common.NS(data))[0] + if sexp[1][0] == 'dsa': + assert len(kd) == 5, len(kd) + return Class(DSA.construct((kd['y'], kd['g'], kd['p'], + kd['q'], kd['x']))) + elif sexp[1][0] == 'rsa-pkcs1': + assert len(kd) == 8, len(kd) + if kd['p'] > kd['q']: # make p smaller than q + kd['p'], kd['q'] = kd['q'], kd['p'] + return Class(RSA.construct((kd['n'], kd['e'], kd['d'], + kd['p'], kd['q']))) + else: + raise BadKeyError('unknown lsh key type %s' % sexp[1][0]) + _fromString_PRIVATE_LSH = classmethod(_fromString_PRIVATE_LSH) + + def _fromString_AGENTV3(Class, data): + """ + Return a private key object corresponsing to the Secure Shell Key + Agent v3 format. + + The SSH Key Agent v3 format for a RSA key is:: + string 'ssh-rsa' + integer e + integer d + integer n + integer u + integer p + integer q + + The SSH Key Agent v3 format for a DSA key is:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + integer x + + @type data: C{str} + @return: a C{Crypto.PublicKey.pubkey.pubkey} object + @raises BadKeyError: if the key type (the first string) is unknown + """ + keyType, data = common.getNS(data) + if keyType == 'ssh-dss': + p, data = common.getMP(data) + q, data = common.getMP(data) + g, data = common.getMP(data) + y, data = common.getMP(data) + x, data = common.getMP(data) + return Class(DSA.construct((y,g,p,q,x))) + elif keyType == 'ssh-rsa': + e, data = common.getMP(data) + d, data = common.getMP(data) + n, data = common.getMP(data) + u, data = common.getMP(data) + p, data = common.getMP(data) + q, data = common.getMP(data) + return Class(RSA.construct((n,e,d,p,q,u))) + else: + raise BadKeyError("unknown key type %s" % keyType) + _fromString_AGENTV3 = classmethod(_fromString_AGENTV3) + + def _guessStringType(Class, data): + """ + Guess the type of key in data. The types map to _fromString_* + methods. + """ + if data.startswith('ssh-'): + return 'public_openssh' + elif data.startswith('-----BEGIN'): + return 'private_openssh' + elif data.startswith('{'): + return 'public_lsh' + elif data.startswith('('): + return 'private_lsh' + elif data.startswith('\x00\x00\x00\x07ssh-'): + ignored, rest = common.getNS(data) + count = 0 + while rest: + count += 1 + ignored, rest = common.getMP(rest) + if count > 4: + return 'agentv3' + else: + return 'blob' + _guessStringType = classmethod(_guessStringType) + + def __init__(self, keyObject): + """ + Initialize a PublicKey with a C{Crypto.PublicKey.pubkey.pubkey} + object. + + @type keyObject: C{Crypto.PublicKey.pubkey.pubkey} + """ + self.keyObject = keyObject + + def __eq__(self, other): + """ + Return True if other represents an object with the same key. + """ + if type(self) == type(other): + return self.type() == other.type() and self.data() == other.data() + else: + return NotImplemented + + def __ne__(self, other): + """ + Return True if other represents anything other than this key. + """ + result = self.__eq__(other) + if result == NotImplemented: + return result + return not result + + def __repr__(self): + """ + Return a pretty representation of this object. + """ + lines = ['<%s %s (%s bits)' % (self.type(), + self.isPublic() and 'Public Key' or 'Private Key', + self.keyObject.size())] + for k, v in self.data().items(): + lines.append('attr %s:' % k) + by = common.MP(v)[4:] + while by: + m = by[:15] + by = by[15:] + o = '' + for c in m: + o = o + '%02x:' % ord(c) + if len(m) < 15: + o = o[:-1] + lines.append('\t' + o) + lines[-1] = lines[-1] + '>' + return '\n'.join(lines) + + def isPublic(self): + """ + Returns True if this Key is a public key. + """ + return not self.keyObject.has_private() + + def public(self): + """ + Returns a version of this key containing only the public key data. + If this is a public key, this may or may not be the same object + as self. + """ + return Key(self.keyObject.publickey()) + + + def fingerprint(self): + """ + Get the user presentation of the fingerprint of this L{Key}. As + described by U{RFC 4716 section + 4}:: + + The fingerprint of a public key consists of the output of the MD5 + message-digest algorithm [RFC1321]. The input to the algorithm is + the public key data as specified by [RFC4253]. (...) The output + of the (MD5) algorithm is presented to the user as a sequence of 16 + octets printed as hexadecimal with lowercase letters and separated + by colons. + + @since: 8.2 + + @return: the user presentation of this L{Key}'s fingerprint, as a + string. + + @rtype: L{str} + """ + return ':'.join([x.encode('hex') for x in md5.md5(self.blob()).digest()]) + + + def type(self): + """ + Return the type of the object we wrap. Currently this can only be + 'RSA' or 'DSA'. + """ + # the class is Crypto.PublicKey.. + klass = str(self.keyObject.__class__) + if klass.startswith('Crypto.PublicKey'): + type = klass.split('.')[2] + else: + raise RuntimeError('unknown type of object: %r' % self.keyObject) + if type in ('RSA', 'DSA'): + return type + else: + raise RuntimeError('unknown type of key: %s' % type) + + def sshType(self): + """ + Return the type of the object we wrap as defined in the ssh protocol. + Currently this can only be 'ssh-rsa' or 'ssh-dss'. + """ + return {'RSA':'ssh-rsa', 'DSA':'ssh-dss'}[self.type()] + + def data(self): + """ + Return the values of the public key as a dictionary. + + @rtype: C{dict} + """ + keyData = {} + for name in self.keyObject.keydata: + value = getattr(self.keyObject, name, None) + if value is not None: + keyData[name] = value + return keyData + + def blob(self): + """ + Return the public key blob for this key. The blob is the + over-the-wire format for public keys: + + RSA keys:: + string 'ssh-rsa' + integer e + integer n + + DSA keys:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + + @rtype: C{str} + """ + type = self.type() + data = self.data() + if type == 'RSA': + return (common.NS('ssh-rsa') + common.MP(data['e']) + + common.MP(data['n'])) + elif type == 'DSA': + return (common.NS('ssh-dss') + common.MP(data['p']) + + common.MP(data['q']) + common.MP(data['g']) + + common.MP(data['y'])) + + def privateBlob(self): + """ + Return the private key blob for this key. The blob is the + over-the-wire format for private keys: + + RSA keys:: + string 'ssh-rsa' + integer n + integer e + integer d + integer u + integer p + integer q + + DSA keys:: + string 'ssh-dss' + integer p + integer q + integer g + integer y + integer x + """ + type = self.type() + data = self.data() + if type == 'RSA': + return (common.NS('ssh-rsa') + common.MP(data['n']) + + common.MP(data['e']) + common.MP(data['d']) + + common.MP(data['u']) + common.MP(data['p']) + + common.MP(data['q'])) + elif type == 'DSA': + return (common.NS('ssh-dss') + common.MP(data['p']) + + common.MP(data['q']) + common.MP(data['g']) + + common.MP(data['y']) + common.MP(data['x'])) + + def toString(self, type, extra=None): + """ + Create a string representation of this key. If the key is a + private key and you want the represenation of its public key, + use .public().toString(). type maps to a _toString_* method. + The extra paramater allows passing data to some of the method. + For public OpenSSH keys, it represents a comment. + For private OpenSSH keys, it represents a passphrase. + + @type type: C{str} + @type extra: C{str} + @rtype: C{str} + """ + method = getattr(self, '_toString_%s' % type.upper(), None) + if method is None: + raise BadKeyError('unknown type: %s' % type) + if method.func_code.co_argcount == 2: + return method(extra) + else: + return method() + + def _toString_OPENSSH(self, extra): + """ + Return a public or private OpenSSH string. See + _fromString_PUBLIC_OPENSSH and _fromString_PRIVATE_OPENSSH for the + string formats. If extra is present, it represents a comment for a + public key, or a passphrase for a private key. + + @type extra: C{str} + @rtype: C{str} + """ + data = self.data() + if self.isPublic(): + b64Data = base64.encodestring(self.blob()).replace('\n', '') + if not extra: + extra = '' + return ('%s %s %s' % (self.sshType(), b64Data, extra)).strip() + else: + lines = ['-----BEGIN %s PRIVATE KEY-----' % self.type()] + if self.type() == 'RSA': + p, q = data['p'], data['q'] + objData = (0, data['n'], data['e'], data['d'], q, p, + data['d'] % (q - 1), data['d'] % (p - 1), + data['u']) + else: + objData = (0, data['p'], data['q'], data['g'], data['y'], + data['x']) + if extra: + iv = randbytes.secureRandom(8) + hexiv = ''.join(['%02X' % ord(x) for x in iv]) + lines.append('Proc-Type: 4,ENCRYPTED') + lines.append('DEK-Info: DES-EDE3-CBC,%s\n' % hexiv) + ba = md5.new(extra + iv).digest() + bb = md5.new(ba + extra + iv).digest() + encKey = (ba + bb)[:24] + asn1Data = asn1.pack([objData]) + if extra: + padLen = 8 - (len(asn1Data) % 8) + asn1Data += (chr(padLen) * padLen) + asn1Data = DES3.new(encKey, DES3.MODE_CBC, + iv).encrypt(asn1Data) + b64Data = base64.encodestring(asn1Data).replace('\n', '') + lines += [b64Data[i:i + 64] for i in range(0, len(b64Data), 64)] + lines.append('-----END %s PRIVATE KEY-----' % self.type()) + return '\n'.join(lines) + + def _toString_LSH(self): + """ + Return a public or private LSH key. See _fromString_PUBLIC_LSH and + _fromString_PRIVATE_LSH for the key formats. + + @rtype: C{str} + """ + data = self.data() + if self.isPublic(): + if self.type() == 'RSA': + keyData = sexpy.pack([['public-key', ['rsa-pkcs1-sha1', + ['n', common.MP(data['n'])[4:]], + ['e', common.MP(data['e'])[4:]]]]]) + elif self.type() == 'DSA': + keyData = sexpy.pack([['public-key', ['dsa', + ['p', common.MP(data['p'])[4:]], + ['q', common.MP(data['q'])[4:]], + ['g', common.MP(data['g'])[4:]], + ['y', common.MP(data['y'])[4:]]]]]) + return '{' + base64.encodestring(keyData).replace('\n', '') + '}' + else: + if self.type() == 'RSA': + p, q = data['p'], data['q'] + return sexpy.pack([['private-key', ['rsa-pkcs1', + ['n', common.MP(data['n'])[4:]], + ['e', common.MP(data['e'])[4:]], + ['d', common.MP(data['d'])[4:]], + ['p', common.MP(q)[4:]], + ['q', common.MP(p)[4:]], + ['a', common.MP(data['d'] % (q - 1))[4:]], + ['b', common.MP(data['d'] % (p - 1))[4:]], + ['c', common.MP(data['u'])[4:]]]]]) + elif self.type() == 'DSA': + return sexpy.pack([['private-key', ['dsa', + ['p', common.MP(data['p'])[4:]], + ['q', common.MP(data['q'])[4:]], + ['g', common.MP(data['g'])[4:]], + ['y', common.MP(data['y'])[4:]], + ['x', common.MP(data['x'])[4:]]]]]) + + def _toString_AGENTV3(self): + """ + Return a private Secure Shell Agent v3 key. See + _fromString_AGENTV3 for the key format. + + @rtype: C{str} + """ + data = self.data() + if not self.isPublic(): + if self.type() == 'RSA': + values = (data['e'], data['d'], data['n'], data['u'], + data['p'], data['q']) + elif self.type() == 'DSA': + values = (data['p'], data['q'], data['g'], data['y'], + data['x']) + return common.NS(self.sshType()) + ''.join(map(common.MP, values)) + + + def sign(self, data): + """ + Returns a signature with this Key. + + @type data: C{str} + @rtype: C{str} + """ + if self.type() == 'RSA': + digest = pkcs1Digest(data, self.keyObject.size()/8) + signature = self.keyObject.sign(digest, '')[0] + ret = common.NS(Util.number.long_to_bytes(signature)) + elif self.type() == 'DSA': + digest = sha.new(data).digest() + randomBytes = randbytes.secureRandom(19) + sig = self.keyObject.sign(digest, randomBytes) + # SSH insists that the DSS signature blob be two 160-bit integers + # concatenated together. The sig[0], [1] numbers from obj.sign + # are just numbers, and could be any length from 0 to 160 bits. + # Make sure they are padded out to 160 bits (20 bytes each) + ret = common.NS(Util.number.long_to_bytes(sig[0], 20) + + Util.number.long_to_bytes(sig[1], 20)) + return common.NS(self.sshType()) + ret + + def verify(self, signature, data): + """ + Returns true if the signature for data is valid for this Key. + + @type signature: C{str} + @type data: C{str} + @rtype: C{bool} + """ + signatureType, signature = common.getNS(signature) + if signatureType != self.sshType(): + return False + if self.type() == 'RSA': + numbers = common.getMP(signature) + digest = pkcs1Digest(data, self.keyObject.size() / 8) + elif self.type() == 'DSA': + signature = common.getNS(signature)[0] + numbers = [Util.number.bytes_to_long(n) for n in signature[:20], + signature[20:]] + digest = sha.new(data).digest() + return self.keyObject.verify(digest, numbers) + +def getPublicKeyString(filename=None, line=0, data=''): + """ + Return a public key string suitable for being sent over the wire. + Takes a filename or data of a public key. Currently handles OpenSSH + and LSH keys. + + This function has been deprecated since Twisted Conch 0.9. Use + Key.fromString() instead. @type filename: C{str} @type line: C{int} @type data: C{str} @rtype: C{str} """ + warnings.warn("getPublicKeyString is deprecated since Twisted Conch 0.9." + " Use Key.fromString().", + DeprecationWarning, stacklevel=2) + if filename and data: + raise BadKeyError("either filename or data, not both") if filename: lines = open(filename).readlines() data = lines[line] - if data[0] == '{': # lsh key - return getPublicKeyString_lsh(data) - elif data.startswith('ssh-'): # openssh key - return getPublicKeyString_openssh(data) - else: - raise BadKeyError('unknown type of key') - -def getPublicKeyString_lsh(data): - sexp = sexpy.parse(base64.decodestring(data[1:-1])) - assert sexp[0] == 'public-key' - kd = {} - for name, data in sexp[1][1:]: - kd[name] = common.NS(data) - if sexp[1][0] == 'dsa': - assert len(kd) == 4, len(kd) - return '\x00\x00\x00\x07ssh-dss' + kd['p'] + kd['q'] + kd['g'] + kd['y'] - elif sexp[1][0] == 'rsa-pkcs1-sha1': - assert len(kd) == 2, len(kd) - return '\x00\x00\x00\x07ssh-rsa' + kd['e'] + kd['n'] - else: - raise BadKeyError('unknown lsh key type %s' % sexp[1][0]) - -def getPublicKeyString_openssh(data): - fileKind, fileData = data.split()[:2] - # if fileKind != kind: - # raise BadKeyError, 'key should be %s but instead is %s' % (kind, fileKind) - return base64.decodestring(fileData) + return Key.fromString(data).blob() -def makePublicKeyString(obj, comment = '', kind = 'openssh'): +def makePublicKeyString(obj, comment='', kind='openssh'): """ Return an public key given a C{Crypto.PublicKey.pubkey.pubkey} object. kind is one of ('openssh', 'lsh') + This function is deprecated since Twisted Conch 0.9. Instead use + Key(obj).toString(). + @type obj: C{Crypto.PublicKey.pubkey.pubkey} @type comment: C{str} @type kind: C{str} @rtype: C{str} """ - - if kind == 'lsh': - return makePublicKeyString_lsh(obj) # no comment - elif kind == 'openssh': - return makePublicKeyString_openssh(obj, comment) - else: - raise BadKeyError('bad kind %s' % kind) - -def makePublicKeyString_lsh(obj): - keyType = objectType(obj) - if keyType == 'ssh-rsa': - keyData = sexpy.pack([['public-key', ['rsa-pkcs1-sha1', - ['n', common.MP(obj.n)[4:]], - ['e', common.MP(obj.e)[4:]]]]]) - elif keyType == 'ssh-dss': - keyData = sexpy.pack([['public-key', ['dsa', - ['p', common.MP(obj.p)[4:]], - ['q', common.MP(obj.q)[4:]], - ['g', common.MP(obj.g)[4:]], - ['y', common.MP(obj.y)[4:]]]]]) - else: - raise BadKeyError('bad keyType %s' % keyType) - return '{' + base64.encodestring(keyData).replace('\n','') + '}' - -def makePublicKeyString_openssh(obj, comment): - keyType = objectType(obj) - if keyType == 'ssh-rsa': - keyData = common.MP(obj.e) + common.MP(obj.n) - elif keyType == 'ssh-dss': - keyData = common.MP(obj.p) - keyData += common.MP(obj.q) - keyData += common.MP(obj.g) - keyData += common.MP(obj.y) - else: - raise BadKeyError('unknown key type %s' % keyType) - b64Data = base64.encodestring(common.NS(keyType)+keyData).replace('\n', '') - return '%s %s %s' % (keyType, b64Data, comment) - + warnings.warn("makePublicKeyString is deprecated since Twisted Conch 0.9." + " Use Key(obj).toString().", + DeprecationWarning, stacklevel=2) + return Key(obj).public().toString(kind, comment) def getPublicKeyObject(data): """ Return a C{Crypto.PublicKey.pubkey.pubkey} corresponding to the SSHv2 public key data. data is in the over-the-wire public key format. + This function is deprecated since Twisted Conch 0.9. Instead, use + Key.fromString(). + @type data: C{str} - @rtype: C{Crypto.PublicKey.pubkey.pubkey} + @rtype: C{Crypto.PublicKey.pubkey.pubkey} """ - keyKind, rest = common.getNS(data) - if keyKind == 'ssh-rsa': - e, rest = common.getMP(rest) - n, rest = common.getMP(rest) - return RSA.construct((n, e)) - elif keyKind == 'ssh-dss': - p, rest = common.getMP(rest) - q, rest = common.getMP(rest) - g, rest = common.getMP(rest) - y, rest = common.getMP(rest) - return DSA.construct((y, g, p, q)) - else: - raise BadKeyError('unknown key type %s' % keyKind) + warnings.warn("getPublicKeyObject is deprecated since Twisted Conch 0.9." + " Use Key.fromString().", + DeprecationWarning, stacklevel=2) + return Key.fromString(data).keyObject -def getPrivateKeyObject(filename = None, data = '', passphrase = ''): +def getPrivateKeyObject(filename=None, data='', passphrase=''): """ Return a C{Crypto.PublicKey.pubkey.pubkey} object corresponding to the private key file/data. If the private key is encrypted, passphrase B{must} be specified, other wise a L{BadKeyError} will be raised. + This method is deprecated since Twisted Conch 0.9. Instead, use + the fromString or fromFile classmethods of Key. + @type filename: C{str} @type data: C{str} @type passphrase: C{str} + @rtype: C{Crypto.PublicKey.pubkey.pubkey} @raises BadKeyError: if the key is invalid or a passphrase is not specified """ + warnings.warn("getPrivateKeyObject is deprecated since Twisted Conch 0.9." + " Use Key.fromString().", + DeprecationWarning, stacklevel=2) + if filename and data: + raise BadKeyError("either filename or data, not both") if filename: - data = open(filename).readlines() - else: - data = [x+'\n' for x in data.split('\n')] - if data[0][0] == '(': # lsh key - return getPrivateKeyObject_lsh(data, passphrase) - elif data[0].startswith('-----'): # openssh key - return getPrivateKeyObject_openssh(data, passphrase) - elif data[0].startswith('ssh-'): # agent v3 private key - return getPrivateKeyObject_agentv3(data, passphrase) - else: - raise BadKeyError('unknown private key type') - -def getPrivateKeyObject_lsh(data, passphrase): - #assert passphrase == '' - data = ''.join(data) - sexp = sexpy.parse(data) - assert sexp[0] == 'private-key' - kd = {} - for name, data in sexp[1][1:]: - kd[name] = common.getMP(common.NS(data))[0] - if sexp[1][0] == 'dsa': - assert len(kd) == 5, len(kd) - return DSA.construct((kd['y'], kd['g'], kd['p'], kd['q'], kd['x'])) - elif sexp[1][0] == 'rsa-pkcs1': - assert len(kd) == 8, len(kd) - return RSA.construct((kd['n'], kd['e'], kd['d'], kd['p'], kd['q'])) - else: - raise BadKeyError('unknown lsh key type %s' % sexp[1][0]) - -def getPrivateKeyObject_openssh(data, passphrase): - kind = data[0][11: 14] - if data[1].startswith('Proc-Type: 4,ENCRYPTED'): # encrypted key - ivdata = data[2].split(',')[1][:-1] - iv = ''.join([chr(int(ivdata[i:i+2],16)) for i in range(0, len(ivdata), 2)]) - if not passphrase: - raise BadKeyError, 'encrypted key with no passphrase' - ba = md5.new(passphrase + iv).digest() - bb = md5.new(ba + passphrase + iv).digest() - decKey = (ba + bb)[:24] - b64Data = base64.decodestring(''.join(data[4:-1])) - keyData = DES3.new(decKey, DES3.MODE_CBC, iv).decrypt(b64Data) - removeLen = ord(keyData[-1]) - keyData = keyData[:-removeLen] - else: - keyData = base64.decodestring(''.join(data[1:-1])) - try: - decodedKey = asn1.parse(keyData) - except Exception, e: - raise BadKeyError, 'something wrong with decode' - if type(decodedKey[0]) == type([]): - decodedKey = decodedKey[0] # this happens with encrypted keys - if kind == 'RSA': - n,e,d,p,q=decodedKey[1:6] - return RSA.construct((n,e,d,p,q)) - elif kind == 'DSA': - p, q, g, y, x = decodedKey[1: 6] - return DSA.construct((y, g, p, q, x)) - -def getPrivateKeyObject_agentv3(data, passphrase): - if passphrase: - raise BadKeyError("agent v3 key should not be encrypted") - keyType, data = common.getNS(data) - if keyType == 'ssh-dss': - p, data = common.getMP(data) - q, data = common.getMP(data) - g, data = common.getMP(data) - y, data = common.getMP(data) - x, data = common.getMP(data) - return DSA.construct((y,g,p,q,x)) - elif keyType == 'ssh-rsa': - e, data = common.getMP(data) - d, data = common.getMP(data) - n, data = common.getMP(data) - u, data = common.getMP(data) - p, data = common.getMP(data) - q, data = common.getMP(data) - return RSA.construct((n,e,d,p,q,u)) + return Key.fromFile(filename, passphrase=passphrase).keyObject else: - raise BadKeyError("unknown key type %s" % keyType) + return Key.fromString(data, passphrase=passphrase).keyObject -def makePrivateKeyString(obj, passphrase = None, kind = 'openssh'): +def makePrivateKeyString(obj, passphrase=None, kind='openssh'): """ Return an OpenSSH-style private key for a C{Crypto.PublicKey.pubkey.pubkey} object. If passphrase is given, encrypt the private key with it. kind is one of ('openssh', 'lsh', 'agentv3') + This function is deprecated since Twisted Conch 0.9. Instead use + Key(obj).toString(). + @type obj: C{Crypto.PublicKey.pubkey.pubkey} @type passphrase: C{str}/C{None} @type kind: C{str} @rtype: C{str} """ - if kind == 'lsh': - return makePrivateKeyString_lsh(obj, passphrase) - elif kind == 'openssh': - return makePrivateKeyString_openssh(obj, passphrase) - elif kind == 'agentv3': - return makePrivateKeyString_agentv3(obj, passphrase) - else: - raise BadKeyError('bad kind %s' % kind) - -def makePrivateKeyString_lsh(obj, passphrase): - if passphrase: - raise BadKeyError("cannot encrypt to lsh format") - keyType = objectType(obj) - if keyType == 'ssh-rsa': - p,q=obj.p,obj.q - if p > q: - (p,q)=(q,p) - return sexpy.pack([['private-key', ['rsa-pkcs1', - ['n', common.MP(obj.n)[4:]], - ['e', common.MP(obj.e)[4:]], - ['d', common.MP(obj.d)[4:]], - ['p', common.MP(q)[4:]], - ['q', common.MP(p)[4:]], - ['a', common.MP(obj.d%(q-1))[4:]], - ['b', common.MP(obj.d%(p-1))[4:]], - ['c', common.MP(Util.number.inverse(p, q))[4:]]]]]) - elif keyType == 'ssh-dss': - return sexpy.pack([['private-key', ['dsa', - ['p', common.MP(obj.p)[4:]], - ['q', common.MP(obj.q)[4:]], - ['g', common.MP(obj.g)[4:]], - ['y', common.MP(obj.y)[4:]], - ['x', common.MP(obj.x)[4:]]]]]) - else: - raise BadKeyError('bad keyType %s' % keyType) + warnings.warn("makePrivateKeyString is deprecated since Twisted Conch 0.9." + " Use Key(obj).toString().", + DeprecationWarning, stacklevel=2) + return Key(obj).toString(kind, passphrase) -def makePrivateKeyString_openssh(obj, passphrase): - keyType = objectType(obj) - if keyType == 'ssh-rsa': - keyData = '-----BEGIN RSA PRIVATE KEY-----\n' - p,q=obj.p,obj.q - if p > q: - (p,q) = (q,p) - # p is less than q - objData = [0, obj.n, obj.e, obj.d, q, p, obj.d%(q-1), obj.d%(p-1),Util.number.inverse(p, q)] - elif keyType == 'ssh-dss': - keyData = '-----BEGIN DSA PRIVATE KEY-----\n' - objData = [0, obj.p, obj.q, obj.g, obj.y, obj.x] - else: - raise BadKeyError('unknown key type %s' % keyType) - if passphrase: - iv = common.entropy.get_bytes(8) - hexiv = ''.join(['%02X' % ord(x) for x in iv]) - keyData += 'Proc-Type: 4,ENCRYPTED\n' - keyData += 'DEK-Info: DES-EDE3-CBC,%s\n\n' % hexiv - ba = md5.new(passphrase + iv).digest() - bb = md5.new(ba + passphrase + iv).digest() - encKey = (ba + bb)[:24] - asn1Data = asn1.pack([objData]) - if passphrase: - padLen = 8 - (len(asn1Data) % 8) - asn1Data += (chr(padLen) * padLen) - asn1Data = DES3.new(encKey, DES3.MODE_CBC, iv).encrypt(asn1Data) - b64Data = base64.encodestring(asn1Data).replace('\n','') - b64Data = '\n'.join([b64Data[i:i+64] for i in range(0,len(b64Data),64)]) - keyData += b64Data + '\n' - if keyType == 'ssh-rsa': - keyData += '-----END RSA PRIVATE KEY-----' - elif keyType == 'ssh-dss': - keyData += '-----END DSA PRIVATE KEY-----' - return keyData - -def makePrivateKeyString_agentv3(obj, passphrase): - if passphrase: - raise BadKeyError("cannot encrypt to agent v3 format") - keyType = objectType(obj) - if keyType == 'ssh-rsa': - values = (obj.e, obj.d, obj.n, obj.u, obj.p, obj.q) - elif keyType == 'ssh-dss': - values = (obj.p, obj.q, obj.g, obj.y, obj.x) - return common.NS(keytype) + ''.join(map(common.MP, values)) - - - def makePublicKeyBlob(obj): - keyType = objectType(obj) - if keyType == 'ssh-rsa': - keyData = common.MP(obj.e) + common.MP(obj.n) - elif keyType == 'ssh-dss': - keyData = common.MP(obj.p) - keyData += common.MP(obj.q) - keyData += common.MP(obj.g) - keyData += common.MP(obj.y) - return common.NS(keyType)+keyData - -def makePrivateKeyBlob(obj): - keyType = objectType(obj) - if keyType == 'ssh-rsa': - return common.NS(keyType) + common.MP(obj.n) + common.MP(obj.e) + \ - common.MP(obj.d) + common.MP(obj.u) + common.MP(obj.q) + \ - common.MP(obj.p) - elif keyType == 'ssh-dss': - return common.NS(keyType) + common.MP(obj.p) + common.MP(obj.q) + \ - common.MP(obj.g) + common.MP(obj.y) + common.MP(obj.x) - else: - raise ValueError('trying to get blob for invalid key type: %s' % keyType) + """ + Make a public key blob from a C{Crypto.PublicKey.pubkey.pubkey}. + + This function is deprecated since Twisted Conch 0.9. Use + Key().blob() instead. + """ + warnings.warn("makePublicKeyBlob is deprecated since Twisted Conch 0.9." + " Use Key(obj).blob().", + DeprecationWarning, stacklevel=2) + return Key(obj).blob() def objectType(obj): """ @@ -371,101 +842,84 @@ ('n', 'e', 'd', 'p', 'q', 'u'): 'ssh-rsa', ('y', 'g', 'p', 'q', 'x'): 'ssh-dss' } - return keyDataMapping[tuple(obj.keydata)] + try: + return keyDataMapping[tuple(obj.keydata)] + except (KeyError, AttributeError): + raise BadKeyError("invalid key object", obj) -def pkcs1Pad(data, lMod): - lenPad = lMod-2-len(data) - return '\x01'+('\xff'*lenPad)+'\x00'+data +def pkcs1Pad(data, messageLength): + """ + Pad out data to messageLength according to the PKCS#1 standard. + @type data: C{str} + @type messageLength: C{int} + """ + lenPad = messageLength - 2 - len(data) + return '\x01' + ('\xff' * lenPad) + '\x00' + data -def pkcs1Digest(data, lMod): +def pkcs1Digest(data, messageLength): + """ + Create a message digest using the SHA1 hash algorithm according to the + PKCS#1 standard. + @type data: C{str} + @type messageLength: C{str} + """ digest = sha.new(data).digest() - return pkcs1Pad(ID_SHA1+digest, lMod) + return pkcs1Pad(ID_SHA1+digest, messageLength) def lenSig(obj): + """ + Return the length of the signature in bytes for a key object. + + @type obj: C{Crypto.PublicKey.pubkey.pubkey} + @rtype: C{long} + """ return obj.size()/8 def signData(obj, data): """ Sign the data with the given C{Crypto.PublicKey.pubkey.pubkey} object. + This method is deprecated since Twisted Conch 0.9. Instead use + Key().sign(). + @type obj: C{Crypto.PublicKey.pubkey.pubkey} @type data: C{str} @rtype: C{str} """ - mapping = { - 'ssh-rsa': signData_rsa, - 'ssh-dss': signData_dsa - } - objType = objectType(obj) - return common.NS(objType)+mapping[objType](obj, data) - -def signData_rsa(obj, data): - sigData = pkcs1Digest(data, lenSig(obj)) - sig = obj.sign(sigData, '')[0] - return common.NS(Util.number.long_to_bytes(sig)) # get around adding the \x00 byte - -def signData_dsa(obj, data): - sigData = sha.new(data).digest() - randData = common.entropy.get_bytes(19) - sig = obj.sign(sigData, randData) - # SSH insists that the DSS signature blob be two 160-bit integers - # concatenated together. The sig[0], [1] numbers from obj.sign are just - # numbers, and could be any length from 0 to 160 bits. Make sure they - # are padded out to 160 bits (20 bytes each) - return common.NS(Util.number.long_to_bytes(sig[0], 20) + - Util.number.long_to_bytes(sig[1], 20)) + warnings.warn("signData is deprecated since Twisted Conch 0.9." + " Use Key(obj).sign(data).", + DeprecationWarning, stacklevel=2) + return Key(obj).sign(data) def verifySignature(obj, sig, data): """ Verify that the signature for the data is valid. + This method is deprecated since Twisted Conch 0.9. Use + Key().verify(). + @type obj: C{Crypto.PublicKey.pubkey.pubkey} @type sig: C{str} @type data: C{str} @rtype: C{bool} """ - mapping = { - 'ssh-rsa': verifySignature_rsa, - 'ssh-dss': verifySignature_dsa, - } - objType = objectType(obj) - sigType, sigData = common.getNS(sig) - if objType != sigType: # object and signature are not of same type - return 0 - return mapping[objType](obj, sigData, data) - -def verifySignature_rsa(obj, sig, data): - sigTuple = [common.getMP(sig)[0]] - return obj.verify(pkcs1Digest(data, lenSig(obj)), sigTuple) - -def verifySignature_dsa(obj, sig, data): - sig = common.getNS(sig)[0] - assert(len(sig) == 40) - l = len(sig)/2 - sigTuple = map(Util.number.bytes_to_long, [sig[: l], sig[l:]]) - return obj.verify(sha.new(data).digest(), sigTuple) + warnings.warn("verifySignature is deprecated since Twisted Conch 0.9." + " Use Key(obj).verify(signature, data).", + DeprecationWarning, stacklevel=2) + return Key(obj).verify(sig, data) def printKey(obj): """ Pretty print a C{Crypto.PublicKey.pubkey.pubkey} object. + This function is deprecated since Twisted Conch 0.9. Use + repr(Key()). + @type obj: C{Crypto.PublicKey.pubkey.pubkey} """ - print '%s %s (%s bits)'%(objectType(obj), - obj.hasprivate()and 'Private Key'or 'Public Key', - obj.size()) - for k in obj.keydata: - if hasattr(obj, k): - print 'attr', k - by = common.MP(getattr(obj, k))[4:] - while by: - m = by[: 15] - by = by[15:] - o = '' - for c in m: - o = o+'%02x:'%ord(c) - if len(m) < 15: - o = o[:-1] - print '\t'+o + warnings.warn("printKey is deprecated since Twisted Conch 0.9." + " Use repr(Key(obj)).", + DeprecationWarning, stacklevel=2) + return repr(Key(obj))[1:-1] ID_SHA1 = '\x30\x21\x30\x09\x06\x05\x2b\x0e\x03\x02\x1a\x05\x00\x04\x14' diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/service.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/service.py --- twisted-conch-0.8.0/twisted/conch/ssh/service.py 2004-08-25 09:36:30.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ssh/service.py 2008-07-29 21:13:54.000000000 +0100 @@ -1,13 +1,11 @@ # Copyright (c) 2001-2004 Twisted Matrix Laboratories. # See LICENSE for details. -# - -"""The parent class for all the SSH services. Currently implemented services are: ssh-userauth and ssh-connection. - -This module is unstable. +""" +The parent class for all the SSH services. Currently implemented services +are ssh-userauth and ssh-connection. -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ @@ -30,18 +28,21 @@ """ def logPrefix(self): - return "SSHService %s on %s" % (self.name, self.transport.transport.logPrefix()) + return "SSHService %s on %s" % (self.name, + self.transport.transport.logPrefix()) - def packetReceived(self, messageType, packet): + def packetReceived(self, messageNum, packet): """ - called when we receieve a packet on the transport + called when we receive a packet on the transport """ #print self.protocolMessages - f = getattr(self,'ssh_%s' % self.protocolMessages[messageType][4:], None) - if f: - f(packet) - else: - log.msg("couldn't handle", messageType) - log.msg(repr(packet[1:])) - self.transport.sendUnimplemented() + if messageNum in self.protocolMessages: + messageType = self.protocolMessages[messageNum] + f = getattr(self,'ssh_%s' % messageType[4:], + None) + if f is not None: + return f(packet) + log.msg("couldn't handle %r" % messageNum) + log.msg(repr(packet)) + self.transport.sendUnimplemented() diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/session.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/session.py --- twisted-conch-0.8.0/twisted/conch/ssh/session.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ssh/session.py 2008-07-29 21:13:54.000000000 +0100 @@ -4,20 +4,19 @@ # -"""This module contains the implementation of SSHSession, which (by default) +""" +This module contains the implementation of SSHSession, which (by default) allows access to a shell and a python interpreter over SSH. -This module is unstable. - -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ import struct -from twisted.internet import protocol, reactor +from twisted.internet import protocol from twisted.python import log from twisted.conch.interfaces import ISession -import common, channel +from twisted.conch.ssh import common, channel class SSHSession(channel.SSHChannel): @@ -78,7 +77,7 @@ term, windowSize, modes = parseRequest_pty_req(data) log.msg('pty request: %s %s' % (term, windowSize)) try: - self.session.getPty(term, windowSize, modes) + self.session.getPty(term, windowSize, modes) except: log.err() return 0 @@ -88,7 +87,6 @@ def request_window_change(self, data): if not self.session: self.session = ISession(self.avatar) - import fcntl, tty winSize = parseRequest_window_change(data) try: self.session.windowChanged(winSize) @@ -122,6 +120,8 @@ def closed(self): if self.session: self.session.closed() + elif self.client: + self.client.transport.loseConnection() #def closeReceived(self): # self.loseConnection() # don't know what to do with this @@ -139,7 +139,7 @@ self.proto = proto def connectionMade(self): self.proto.connectionMade() - + def outReceived(self, data): self.proto.dataReceived(data) def processEnded(self, reason): self.proto.connectionLost(reason) @@ -160,7 +160,7 @@ def loseConnection(self): self.proto.connectionLost(protocol.connectionDone) - + def wrapProcessProtocol(inst): if isinstance(inst, protocol.Protocol): return _ProtocolWrapper(inst) @@ -194,7 +194,7 @@ self.session.loseConnection() def processEnded(self, reason = None): - if reason and hasattr(reason.value, 'exitCode'): + if reason and hasattr(reason.value, 'exitCode'): log.msg('exitCode: %s' % repr(reason.value.exitCode)) self.session.conn.sendRequest(self.session, 'exit-status', struct.pack('!L', reason.value.exitCode)) self.session.loseConnection() diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/transport.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/transport.py --- twisted-conch-0.8.0/twisted/conch/ssh/transport.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ssh/transport.py 2008-07-29 21:13:54.000000000 +0100 @@ -1,77 +1,166 @@ -# -*- test-case-name: twisted.conch.test.test_conch -*- +# -*- test-case-name: twisted.conch.test.test_transport -*- # -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE for details. -# - -"""The lowest level SSH protocol. This handles the key negotiation, the encryption and the compression. - -This module is unstable. - -Maintainer: U{Paul Swartz} """ +The lowest level SSH protocol. This handles the key negotiation, the +encryption and the compression. The transport layer is described in +RFC 4253. -from __future__ import nested_scopes +Maintainer: Paul Swartz +""" # base library imports import struct import md5 import sha import zlib -import math # for math.log import array # external library imports from Crypto import Util from Crypto.Cipher import XOR -from Crypto.PublicKey import RSA -from Crypto.Util import randpool # twisted imports -from twisted.conch import error from twisted.internet import protocol, defer -from twisted.python import log +from twisted.conch import error +from twisted.python import log, randbytes + +# sibling imports +from twisted.conch.ssh import keys +from twisted.conch.ssh.common import NS, getNS, MP, getMP, _MPpow, ffs -# sibling importsa -from common import NS, getNS, MP, getMP, _MPpow, ffs, entropy # ease of use -import keys class SSHTransportBase(protocol.Protocol): + """ + Protocol supporting basic SSH functionality: sending/receiving packets + and message dispatch. To connect to or run a server, you must use + SSHClientTransport or SSHServerTransport. + + @ivar protocolVersion: A string representing the version of the SSH + protocol we support. Currently defaults to '2.0'. + + @ivar version: A string representing the version of the server or client. + Currently defaults to 'Twisted'. + + @ivar comment: An optional string giving more information about the + server or client. + + @ivar supportedCiphers: A list of strings representing the encryption + algorithms supported, in order from most-preferred to least. + + @ivar supportedMACs: A list of strings representing the message + authentication codes (hashes) supported, in order from most-preferred + to least. Both this and supportedCiphers can include 'none' to use + no encryption or authentication, but that must be done manually, + + @ivar supportedKeyExchanges: A list of strings representing the + key exchanges supported, in order from most-preferred to least. + + @ivar supportedPublicKeys: A list of strings representing the + public key types supported, in order from most-preferred to least. + + @ivar supportedCompressions: A list of strings representing compression + types supported, from most-preferred to least. + + @ivar supportedLanguages: A list of strings representing languages + supported, from most-preferred to least. + + @ivar isClient: A boolean indicating whether this is a client or server. + + @ivar gotVersion: A boolean indicating whether we have receieved the + version string from the other side. + + @ivar buf: Data we've received but hasn't been parsed into a packet. + + @ivar outgoingPacketSequence: the sequence number of the next packet we + will send. + + @ivar incomingPacketSequence: the sequence number of the next packet we + are expecting from the other side. + + @ivar outgoingCompression: an object supporting the .compress(str) and + .flush() methods, or None if there is no outgoing compression. Used to + compress outgoing data. + + @ivar outgoingCompressionType: A string representing the outgoing + compression type. + + @ivar incomingCompression: an object supporting the .decompress(str) + method, or None if there is no incoming compression. Used to + decompress incoming data. + + @ivar incomingCompressionType: A string representing the incoming + compression type. + + @ivar ourVersionString: the version string that we sent to the other side. + Used in the key exchange. + + @ivar otherVersionString: the version string sent by the other side. Used + in the key exchange. + + @ivar ourKexInitPayload: the MSG_KEXINIT payload we sent. Used in the key + exchange. + + @ivar otherKexInitPayload: the MSG_KEXINIT payload we received. Used in + the key exchange + + @ivar sessionID: a string that is unique to this SSH session. Created as + part of the key exchange, sessionID is used to generate the various + encryption and authentication keys. + + @ivar service: an SSHService instance, or None. If it's set to an object, + it's the currently running service. + + @ivar kexAlg: the agreed-upon key exchange algorithm. + + @ivar keyAlg: the agreed-upon public key type for the key exchange. + + @ivar currentEncryptions: an SSHCiphers instance. It represents the + current encryption and authentication options for the transport. + + @ivar nextEncryptions: an SSHCiphers instance. Held here until the + MSG_NEWKEYS messages are exchanged, when nextEncryptions is + transitioned to currentEncryptions. + + @ivar first: the first bytes of the next packet. In order to avoid + decrypting data twice, the first bytes are decrypted and stored until + the whole packet is available. + + """ + + protocolVersion = '2.0' version = 'Twisted' comment = '' - ourVersionString = ('SSH-'+protocolVersion+'-'+version+' '+comment).strip() - - supportedCiphers = ['aes256-ctr', 'aes256-cbc', 'aes192-ctr', 'aes192-cbc', - 'aes128-ctr', 'aes128-cbc', 'cast128-ctr', - 'cast128-cbc', 'blowfish-ctr', 'blowfish', 'idea-ctr' - 'idea-cbc', '3des-ctr', '3des-cbc'] # ,'none'] + ourVersionString = ('SSH-' + protocolVersion + '-' + version + ' ' + + comment).strip() + supportedCiphers = ['aes256-ctr', 'aes256-cbc', 'aes192-ctr', 'aes192-cbc', + 'aes128-ctr', 'aes128-cbc', 'cast128-ctr', + 'cast128-cbc', 'blowfish-ctr', 'blowfish-cbc', + '3des-ctr', '3des-cbc'] # ,'none'] supportedMACs = ['hmac-sha1', 'hmac-md5'] # , 'none'] - # both of the above support 'none', but for security are disabled by # default. to enable them, subclass this class and add it, or do: # SSHTransportBase.supportedCiphers.append('none') - - supportedKeyExchanges = ['diffie-hellman-group-exchange-sha1', + supportedKeyExchanges = ['diffie-hellman-group-exchange-sha1', 'diffie-hellman-group1-sha1'] supportedPublicKeys = ['ssh-rsa', 'ssh-dss'] supportedCompressions = ['none', 'zlib'] supportedLanguages = () - - gotVersion = 0 - ignoreNextPacket = 0 + isClient = False + gotVersion = False buf = '' outgoingPacketSequence = 0 incomingPacketSequence = 0 - currentEncryptions = None outgoingCompression = None incomingCompression = None sessionID = None - isAuthorized = 0 service = None + def connectionLost(self, reason): if self.service: self.service.serviceStopped() @@ -79,299 +168,618 @@ self.logoutFunction() log.msg('connection lost') + def connectionMade(self): - self.transport.write('%s\r\n'%(self.ourVersionString)) + """ + Called when the connection is made to the other side. We sent our + version and the MSG_KEXINIT packet. + """ + self.transport.write('%s\r\n' % (self.ourVersionString,)) + self.currentEncryptions = SSHCiphers('none', 'none', 'none', 'none') + self.currentEncryptions.setKeys('', '', '', '', '', '') self.sendKexInit() + def sendKexInit(self): - self.ourKexInitPayload = chr(MSG_KEXINIT)+entropy.get_bytes(16)+ \ - NS(','.join(self.supportedKeyExchanges))+ \ - NS(','.join(self.supportedPublicKeys))+ \ - NS(','.join(self.supportedCiphers))+ \ - NS(','.join(self.supportedCiphers))+ \ - NS(','.join(self.supportedMACs))+ \ - NS(','.join(self.supportedMACs))+ \ - NS(','.join(self.supportedCompressions))+ \ - NS(','.join(self.supportedCompressions))+ \ - NS(','.join(self.supportedLanguages))+ \ - NS(','.join(self.supportedLanguages))+ \ - '\000'+'\000\000\000\000' + self.ourKexInitPayload = (chr(MSG_KEXINIT) + + randbytes.secureRandom(16) + + NS(','.join(self.supportedKeyExchanges)) + + NS(','.join(self.supportedPublicKeys)) + + NS(','.join(self.supportedCiphers)) + + NS(','.join(self.supportedCiphers)) + + NS(','.join(self.supportedMACs)) + + NS(','.join(self.supportedMACs)) + + NS(','.join(self.supportedCompressions)) + + NS(','.join(self.supportedCompressions)) + + NS(','.join(self.supportedLanguages)) + + NS(','.join(self.supportedLanguages)) + + '\000' + '\000\000\000\000') self.sendPacket(MSG_KEXINIT, self.ourKexInitPayload[1:]) + def sendPacket(self, messageType, payload): - payload = chr(messageType)+payload + """ + Sends a packet. If it's been set up, compress the data, encrypt it, + and authenticate it before sending. + + @param messageType: The type of the packet; generally one of the + MSG_* values. + @type messageType: C{int} + @param payload: The payload for the message. + @type payload: C{str} + """ + payload = chr(messageType) + payload if self.outgoingCompression: - payload = self.outgoingCompression.compress(payload) + self.outgoingCompression.flush(2) - if self.currentEncryptions: - bs = self.currentEncryptions.enc_block_size - else: - bs = 8 - totalSize = 5+len(payload) - lenPad = bs-(totalSize%bs) + payload = (self.outgoingCompression.compress(payload) + + self.outgoingCompression.flush(2)) + bs = self.currentEncryptions.encBlockSize + # 4 for the packet length and 1 for the padding length + totalSize = 5 + len(payload) + lenPad = bs - (totalSize % bs) if lenPad < 4: - lenPad = lenPad+bs - packet = struct.pack('!LB', totalSize+lenPad-4, lenPad)+ \ - payload+entropy.get_bytes(lenPad) - assert len(packet)%bs == 0, '%s extra bytes in packet'%(len(packet)%bs) - if self.currentEncryptions: - encPacket = self.currentEncryptions.encrypt(packet) + self.currentEncryptions.makeMAC(self.outgoingPacketSequence, packet) - else: - encPacket = packet + lenPad = lenPad + bs + packet = (struct.pack('!LB', + totalSize + lenPad - 4, lenPad) + + payload + randbytes.secureRandom(lenPad)) + encPacket = ( + self.currentEncryptions.encrypt(packet) + + self.currentEncryptions.makeMAC( + self.outgoingPacketSequence, packet)) self.transport.write(encPacket) - self.outgoingPacketSequence+=1 + self.outgoingPacketSequence += 1 + def getPacket(self): - bs = self.currentEncryptions and self.currentEncryptions.dec_block_size or 8 - ms = self.currentEncryptions and self.currentEncryptions.verify_digest_size or 0 + """ + Try to return a decrypted, authenticated, and decompressed packet + out of the buffer. If there is not enough data, return None. + + @rtype: C{str}/C{None} + """ + bs = self.currentEncryptions.decBlockSize + ms = self.currentEncryptions.verifyDigestSize if len(self.buf) < bs: return # not enough data if not hasattr(self, 'first'): - if self.currentEncryptions: - first = self.currentEncryptions.decrypt(self.buf[: bs]) - else: - first = self.buf[: bs] + first = self.currentEncryptions.decrypt(self.buf[:bs]) else: first = self.first del self.first - packetLen, randomLen = struct.unpack('!LB', first[: 5]) + packetLen, paddingLen = struct.unpack('!LB', first[:5]) if packetLen > 1048576: # 1024 ** 2 - self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, 'bad packet length %s'%packetLen) + self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, + 'bad packet length %s' % packetLen) return - if len(self.buf) < packetLen+4+ms: + if len(self.buf) < packetLen + 4 + ms: self.first = first return # not enough packet - if(packetLen+4)%bs != 0: - self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, 'bad packet mod (%s%%%s == %s'%(packetLen+4, bs, (packetLen+4)%bs)) + if(packetLen + 4) % bs != 0: + self.sendDisconnect( + DISCONNECT_PROTOCOL_ERROR, + 'bad packet mod (%i%%%i == %i)' % (packetLen + 4, bs, + (packetLen + 4) % bs)) return - encData, self.buf = self.buf[: 4+packetLen], self.buf[4+packetLen:] - if self.currentEncryptions: - packet = first+self.currentEncryptions.decrypt(encData[bs:]) - else: - packet = encData - if len(packet) != 4+packetLen: - self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, 'bad packet length') - return - if ms: - macData, self.buf = self.buf[:ms], self.buf[ms:] - if not self.currentEncryptions.verify(self.incomingPacketSequence, packet, macData): + encData, self.buf = self.buf[:4 + packetLen], self.buf[4 + packetLen:] + packet = first + self.currentEncryptions.decrypt(encData[bs:]) + if len(packet) != 4 + packetLen: + self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, + 'bad decryption') + return + if ms: + macData, self.buf = self.buf[:ms], self.buf[ms:] + if not self.currentEncryptions.verify(self.incomingPacketSequence, + packet, macData): self.sendDisconnect(DISCONNECT_MAC_ERROR, 'bad MAC') return - payload = packet[5: 4+packetLen-randomLen] + payload = packet[5:-paddingLen] if self.incomingCompression: try: payload = self.incomingCompression.decompress(payload) - except zlib.error: - self.sendDisconnect(DISCONNECT_COMPRESSION_ERROR, 'compression error') + except: # bare except, because who knows what kind of errors + # decompression can raise + log.err() + self.sendDisconnect(DISCONNECT_COMPRESSION_ERROR, + 'compression error') return - self.incomingPacketSequence+=1 + self.incomingPacketSequence += 1 return payload + def dataReceived(self, data): - self.buf = self.buf+data + """ + First, check for the version string (SSH-2.0-*). After that has been + received, this method adds data to the buffer, and pulls out any + packets. + + @type data: C{str} + """ + self.buf = self.buf + data if not self.gotVersion: - parts = self.buf.split('\n') - for p in parts: - if p[: 4] == 'SSH-': - self.gotVersion = 1 + if self.buf.find('\n', self.buf.find('SSH-')) == -1: + return + lines = self.buf.split('\n') + for p in lines: + if p.startswith('SSH-'): + self.gotVersion = True self.otherVersionString = p.strip() - if p.split('-')[1]not in('1.99', '2.0'): # bad version - self.sendDisconnect(DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, 'bad version %s'%p.split('-')[1]) + if p.split('-')[1] not in ('1.99', '2.0'): # bad version + self.sendDisconnect( + DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, + 'bad version ' + p.split('-')[1]) return - i = parts.index(p) - self.buf = '\n'.join(parts[i+1:]) + i = lines.index(p) + self.buf = '\n'.join(lines[i + 1:]) packet = self.getPacket() while packet: messageNum = ord(packet[0]) - if messageNum < 50: - messageType = messages[messageNum][4:] - f = getattr(self, 'ssh_%s'%messageType, None) - if f: - f(packet[1:]) - else: - log.msg("couldn't handle %s"%messageType) - log.msg(repr(packet[1:])) - self.sendUnimplemented() - elif self.service: - log.callWithLogger(self.service, self.service.packetReceived, - ord(packet[0]), packet[1:]) + self.dispatchMessage(messageNum, packet[1:]) + packet = self.getPacket() + + + def dispatchMessage(self, messageNum, payload): + """ + Send a received message to the appropriate method. + + @type messageNum: C{int} + @type payload: c{str} + """ + if messageNum < 50 and messageNum in messages: + messageType = messages[messageNum][4:] + f = getattr(self, 'ssh_%s' % messageType, None) + if f is not None: + f(payload) else: - log.msg("couldn't handle %s"%messageNum) - log.msg(repr(packet[1:])) + log.msg("couldn't handle %s" % messageType) + log.msg(repr(payload)) self.sendUnimplemented() - packet = self.getPacket() + elif self.service: + log.callWithLogger(self.service, self.service.packetReceived, + messageNum, payload) + else: + log.msg("couldn't handle %s" % messageNum) + log.msg(repr(payload)) + self.sendUnimplemented() + + + def ssh_KEXINIT(self, packet): + """ + Called when we receive a MSG_KEXINIT message. Payload:: + bytes[16] cookie + string keyExchangeAlgorithms + string keyAlgorithms + string incomingEncryptions + string outgoingEncryptions + string incomingAuthentications + string outgoingAuthentications + string incomingCompressions + string outgoingCompressions + string incomingLanguages + string outgoingLanguages + bool firstPacketFollows + unit32 0 (reserved) + + Starts setting up the key exchange, keys, encryptions, and + authentications. Extended by ssh_KEXINIT in SSHServerTransport and + SSHClientTransport. + """ + self.otherKexInitPayload = chr(MSG_KEXINIT) + packet + #cookie = packet[: 16] # taking this is useless + k = getNS(packet[16:], 10) + strings, rest = k[:-1], k[-1] + (kexAlgs, keyAlgs, encCS, encSC, macCS, macSC, compCS, compSC, langCS, + langSC) = [s.split(',') for s in strings] + # these are the server directions + outs = [encSC, macSC, compSC] + ins = [encCS, macSC, compCS] + if self.isClient: + outs, ins = ins, outs # switch directions + server = (self.supportedKeyExchanges, self.supportedPublicKeys, + self.supportedCiphers, self.supportedCiphers, + self.supportedMACs, self.supportedMACs, + self.supportedCompressions, self.supportedCompressions) + client = (kexAlgs, keyAlgs, outs[0], ins[0], outs[1], ins[1], + outs[2], ins[2]) + if self.isClient: + server, client = client, server + self.kexAlg = ffs(client[0], server[0]) + self.keyAlg = ffs(client[1], server[1]) + self.nextEncryptions = SSHCiphers( + ffs(client[2], server[2]), + ffs(client[3], server[3]), + ffs(client[4], server[4]), + ffs(client[5], server[5])) + self.outgoingCompressionType = ffs(client[6], server[6]) + self.incomingCompressionType = ffs(client[7], server[7]) + if None in (self.kexAlg, self.keyAlg, self.outgoingCompressionType, + self.incomingCompressionType): + self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, + "couldn't match all kex parts") + return + if None in self.nextEncryptions.__dict__.values(): + self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, + "couldn't match all kex parts") + return + log.msg('kex alg, key alg: %s %s' % (self.kexAlg, self.keyAlg)) + log.msg('outgoing: %s %s %s' % (self.nextEncryptions.outCipType, + self.nextEncryptions.outMACType, + self.outgoingCompressionType)) + log.msg('incoming: %s %s %s' % (self.nextEncryptions.inCipType, + self.nextEncryptions.inMACType, + self.incomingCompressionType)) + return kexAlgs, keyAlgs, rest # for SSHServerTransport to use + def ssh_DISCONNECT(self, packet): + """ + Called when we receive a MSG_DISCONNECT message. Payload:: + long code + string description + + This means that the other side has disconnected. Pass the message up + and disconnect ourselves. + """ reasonCode = struct.unpack('>L', packet[: 4])[0] description, foo = getNS(packet[4:]) self.receiveError(reasonCode, description) self.transport.loseConnection() - def ssh_IGNORE(self, packet): pass + + def ssh_IGNORE(self, packet): + """ + Called when we receieve a MSG_IGNORE message. No payload. + This means nothing; we simply return. + """ + def ssh_UNIMPLEMENTED(self, packet): - seqnum = struct.unpack('>L', packet) + """ + Called when we receieve a MSG_UNIMPLEMENTED message. Payload:: + long packet + + This means that the other side did not implement one of our packets. + """ + seqnum, = struct.unpack('>L', packet) self.receiveUnimplemented(seqnum) + def ssh_DEBUG(self, packet): - alwaysDisplay = ord(packet[0]) - message, lang, foo = getNS(packet, 2) + """ + Called when we receieve a MSG_DEBUG message. Payload:: + bool alwaysDisplay + string message + string language + + This means the other side has passed along some debugging info. + """ + alwaysDisplay = bool(packet[0]) + message, lang, foo = getNS(packet[1:], 2) self.receiveDebug(alwaysDisplay, message, lang) + def setService(self, service): - log.msg('starting service %s'%service.name) + """ + Set our service to service and start it running. If we were + running a service previously, stop it first. + + @type service: C{SSHService} + """ + log.msg('starting service %s' % service.name) if self.service: self.service.serviceStopped() self.service = service service.transport = self self.service.serviceStarted() - def sendDebug(self, message, alwaysDisplay = 0, language = ''): - self.sendPacket(MSG_DEBUG, chr(alwaysDisplay)+NS(message)+NS(language)) + + def sendDebug(self, message, alwaysDisplay=False, language=''): + """ + Send a debug message to the other side. + + @param message: the message to send. + @type message: C{str} + @param alwaysDisplay: if True, tell the other side to always + display this message. + @type alwaysDisplay: C{bool} + @param language: optionally, the language the message is in. + @type language: C{str} + """ + self.sendPacket(MSG_DEBUG, chr(alwaysDisplay) + NS(message) + + NS(language)) + def sendIgnore(self, message): + """ + Send a message that will be ignored by the other side. This is + useful to fool attacks based on guessing packet sizes in the + encrypted stream. + + @param message: data to send with the message + @type message: C{str} + """ self.sendPacket(MSG_IGNORE, NS(message)) + def sendUnimplemented(self): + """ + Send a message to the other side that the last packet was not + understood. + """ seqnum = self.incomingPacketSequence self.sendPacket(MSG_UNIMPLEMENTED, struct.pack('!L', seqnum)) + def sendDisconnect(self, reason, desc): - self.sendPacket(MSG_DISCONNECT, struct.pack('>L', reason)+NS(desc)+NS('')) - log.msg('Disconnecting with error, code %s\nreason: %s'%(reason, desc)) + """ + Send a disconnect message to the other side and then disconnect. + + @param reason: the reason for the disconnect. Should be one of the + DISCONNECT_* values. + @type reason: C{int} + @param desc: a descrption of the reason for the disconnection. + @type desc: C{str} + """ + self.sendPacket( + MSG_DISCONNECT, struct.pack('>L', reason) + NS(desc) + NS('')) + log.msg('Disconnecting with error, code %s\nreason: %s' % (reason, + desc)) self.transport.loseConnection() - # client methods - def receiveError(self, reasonCode, description): - log.msg('Got remote error, code %s\nreason: %s'%(reasonCode, description)) - def receiveUnimplemented(self, seqnum): - log.msg('other side unimplemented packet #%s'%seqnum) + def _getKey(self, c, sharedSecret, exchangeHash): + """ + Get one of the keys for authentication/encryption. - def receiveDebug(self, alwaysDisplay, message, lang): - if alwaysDisplay: - log.msg('Remote Debug Message:', message) + @type c: C{str} + @type sharedSecret: C{str} + @type exchangeHash: C{str} + """ + k1 = sha.new(sharedSecret + exchangeHash + c + self.sessionID) + k1 = k1.digest() + k2 = sha.new(sharedSecret + exchangeHash + k1).digest() + return k1 + k2 - def isEncrypted(self, direction = "out"): - """direction must be in ["out", "in", "both"] + + def _keySetup(self, sharedSecret, exchangeHash): """ - if self.currentEncryptions == None: - return 0 - elif direction == "out": - return bool(self.currentEncryptions.enc_block_size) + Set up the keys for the connection and sends MSG_NEWKEYS when + finished, + + @param sharedSecret: a secret string agreed upon using a Diffie- + Hellman exchange, so it is only shared between + the server and the client. + @type sharedSecret: C{str} + @param exchangeHash: A hash of various data known by both sides. + @type exchangeHash: C{str} + """ + if not self.sessionID: + self.sessionID = exchangeHash + initIVCS = self._getKey('A', sharedSecret, exchangeHash) + initIVSC = self._getKey('B', sharedSecret, exchangeHash) + encKeyCS = self._getKey('C', sharedSecret, exchangeHash) + encKeySC = self._getKey('D', sharedSecret, exchangeHash) + integKeyCS = self._getKey('E', sharedSecret, exchangeHash) + integKeySC = self._getKey('F', sharedSecret, exchangeHash) + outs = [initIVSC, encKeySC, integKeySC] + ins = [initIVCS, encKeyCS, integKeyCS] + if self.isClient: # reverse for the client + log.msg('REVERSE') + outs, ins = ins, outs + self.nextEncryptions.setKeys(outs[0], outs[1], ins[0], ins[1], + outs[2], ins[2]) + self.sendPacket(MSG_NEWKEYS, '') + + + def isEncrypted(self, direction="out"): + """ + Return True if the connection is encrypted in the given direction. + Direction must be one of ["out", "in", "both"]. + """ + if direction == "out": + return self.currentEncryptions.outCipType != 'none' elif direction == "in": - return bool(self.currentEncryptions.dec_block_size) + return self.currentEncryptions.inCipType != 'none' elif direction == "both": return self.isEncrypted("in") and self.isEncrypted("out") else: - raise TypeError, 'direction must be "out", "in", or "both"' + raise TypeError('direction must be "out", "in", or "both"') + - def isVerified(self, direction = "out"): - """direction must be in ["out", "in", "both"] + def isVerified(self, direction="out"): + """ + Return True if the connecction is verified/authenticated in the + given direction. Direction must be one of ["out", "in", "both"]. """ - if self.currentEncryptions == None: - return 0 - elif direction == "out": - return self.currentEncryptions.outMAC != None + if direction == "out": + return self.currentEncryptions.outMACType != 'none' elif direction == "in": - return self.currentEncryptions.outCMAC != None + return self.currentEncryptions.inMACType != 'none' elif direction == "both": return self.isVerified("in")and self.isVerified("out") else: - raise TypeError, 'direction must be "out", "in", or "both"' + raise TypeError('direction must be "out", "in", or "both"') + def loseConnection(self): - self.sendDisconnect(DISCONNECT_CONNECTION_LOST, "user closed connection") + """ + Lose the connection to the other side, sending a + DISCONNECT_CONNECTION_LOST message. + """ + self.sendDisconnect(DISCONNECT_CONNECTION_LOST, + "user closed connection") + + + # client methods + def receiveError(self, reasonCode, description): + """ + Called when we receive a disconnect error message from the other + side. + + @param reasonCode: the reason for the disconnect, one of the + DISCONNECT_ values. + @type reasonCode: C{int} + @param description: a human-readable description of the + disconnection. + @type description: C{str} + """ + log.msg('Got remote error, code %s\nreason: %s' % (reasonCode, + description)) + + + def receiveUnimplemented(self, seqnum): + """ + Called when we receive an unimplemented packet message from the other + side. + + @param seqnum: the sequence number that was not understood. + @type seqnum: C{int} + """ + log.msg('other side unimplemented packet #%s' % seqnum) + + + def receiveDebug(self, alwaysDisplay, message, lang): + """ + Called when we receive a debug message from the other side. + + @param alwaysDisplay: if True, this message should always be + displayed. + @type alwaysDisplay: C{bool} + @param message: the debug message + @type message: C{str} + @param lang: optionally the language the message is in. + @type lang: C{str} + """ + if alwaysDisplay: + log.msg('Remote Debug Message: %s' % message) + + class SSHServerTransport(SSHTransportBase): - isClient = 0 + """ + SSHServerTransport implements the server side of the SSH protocol. + + @ivar isClient: since we are never the client, this is always False. + + @ivar ignoreNextPacket: if True, ignore the next key exchange packet. This + is set when the client sends a guessed key exchange packet but with + an incorrect guess. + + @ivar dhGexRequest: the KEX_DH_GEX_REQUEST(_OLD) that the client sent. + The key generation needs this to be stored. + + @ivar g: the Diffie-Hellman group generator. + + @ivar p: the Diffie-Hellman group prime. + """ + isClient = False + ignoreNextPacket = 0 + + def ssh_KEXINIT(self, packet): - self.clientKexInitPayload = chr(MSG_KEXINIT)+packet - #cookie = packet[: 16] # taking this is useless - k = getNS(packet[16:], 10) - strings, rest = k[:-1], k[-1] - kexAlgs, keyAlgs, encCS, encSC, macCS, macSC, compCS, compSC, langCS, langSC = \ - [s.split(',')for s in strings] - if ord(rest[0]): # first_kex_packet_follows - if kexAlgs[0] != self.supportedKeyExchanges[0]or \ - keyAlgs[0] != self.supportedPublicKeys[0]or \ - not ffs(encSC, self.supportedCiphers)or \ - not ffs(encCS, self.supportedCiphers)or \ - not ffs(macSC, self.supportedMACs)or \ - not ffs(macCS, self.supportedMACs)or \ - not ffs(compCS, self.supportedCompressions)or \ - not ffs(compSC, self.supportedCompressions): - self.ignoreNextPacket = 1 # guess was wrong - self.kexAlg = ffs(kexAlgs, self.supportedKeyExchanges) - self.keyAlg = ffs(keyAlgs, self.supportedPublicKeys) - self.nextEncryptions = SSHCiphers( - ffs(encSC, self.supportedCiphers), - ffs(encCS, self.supportedCiphers), - ffs(macSC, self.supportedMACs), - ffs(macCS, self.supportedMACs), - ) - self.outgoingCompressionType = ffs(compSC, self.supportedCompressions) - self.incomingCompressionType = ffs(compCS, self.supportedCompressions) - if None in(self.kexAlg, self.keyAlg, self.outgoingCompressionType, self.incomingCompressionType): - self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, "couldn't match all kex parts") - return - if None in self.nextEncryptions.__dict__.values(): - self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, "couldn't match all kex parts") + """ + Called when we receive a MSG_KEXINIT message. For a description + of the packet, see SSHTransportBase.ssh_KEXINIT(). Additionally, + this method checks if a guessed key exchange packet was sent. If + it was sent, and it guessed incorrectly, the next key exchange + packet MUST be ignored. + """ + retval = SSHTransportBase.ssh_KEXINIT(self, packet) + if not retval: # disconnected return - log.msg('kex alg, key alg: %s %s'%(self.kexAlg, self.keyAlg)) - log.msg('server->client: %s %s %s'%(self.nextEncryptions.outCipType, - self.nextEncryptions.outMacType, - self.outgoingCompressionType)) - log.msg('client->server: %s %s %s'%(self.nextEncryptions.inCipType, - self.nextEncryptions.inMacType, - self.incomingCompressionType)) + else: + kexAlgs, keyAlgs, rest = retval + if ord(rest[0]): # first_kex_packet_follows + if (kexAlgs[0] != self.supportedKeyExchanges[0] or + keyAlgs[0] != self.supportedPublicKeys[0]): + self.ignoreNextPacket = True # guess was wrong def ssh_KEX_DH_GEX_REQUEST_OLD(self, packet): + """ + This represents two different key exchange methods that share the + same integer value. + + KEXDH_INIT (for diffie-hellman-group1-sha1 exchanges) payload:: + + integer e (the client's Diffie-Hellman public key) + + We send the KEXDH_REPLY with our host key and signature. + + KEX_DH_GEX_REQUEST_OLD (for diffie-hellman-group-exchange-sha1) + payload:: + + integer ideal (ideal size for the Diffie-Hellman prime) + + We send the KEX_DH_GEX_GROUP message with the group that is + closest in size to ideal. + + If we were told to ignore the next key exchange packet by + ssh_KEXINIT, drop it on the floor and return. + """ if self.ignoreNextPacket: self.ignoreNextPacket = 0 return - if self.kexAlg == 'diffie-hellman-group1-sha1': # this is really KEXDH_INIT - clientDHPubKey, foo = getMP(packet) - y = Util.number.getRandomNumber(16, entropy.get_bytes) - f = pow(DH_GENERATOR, y, DH_PRIME) - sharedSecret = _MPpow(clientDHPubKey, y, DH_PRIME) + if self.kexAlg == 'diffie-hellman-group1-sha1': + # this is really KEXDH_INIT + clientDHpublicKey, foo = getMP(packet) + y = Util.number.getRandomNumber(512, randbytes.secureRandom) + serverDHpublicKey = _MPpow(DH_GENERATOR, y, DH_PRIME) + sharedSecret = _MPpow(clientDHpublicKey, y, DH_PRIME) h = sha.new() h.update(NS(self.otherVersionString)) h.update(NS(self.ourVersionString)) - h.update(NS(self.clientKexInitPayload)) + h.update(NS(self.otherKexInitPayload)) h.update(NS(self.ourKexInitPayload)) - h.update(NS(self.factory.publicKeys[self.keyAlg])) - h.update(MP(clientDHPubKey)) - h.update(MP(f)) + h.update(NS(self.factory.publicKeys[self.keyAlg].blob())) + h.update(MP(clientDHpublicKey)) + h.update(serverDHpublicKey) h.update(sharedSecret) exchangeHash = h.digest() - self.sendPacket(MSG_KEXDH_REPLY, NS(self.factory.publicKeys[self.keyAlg])+ \ - MP(f)+NS(keys.signData(self.factory.privateKeys[self.keyAlg], exchangeHash))) + self.sendPacket( + MSG_KEXDH_REPLY, + NS(self.factory.publicKeys[self.keyAlg].blob()) + + serverDHpublicKey + + NS(self.factory.privateKeys[self.keyAlg].sign(exchangeHash))) self._keySetup(sharedSecret, exchangeHash) elif self.kexAlg == 'diffie-hellman-group-exchange-sha1': - self.kexAlg = 'diffie-hellman-group-exchange-sha1-old' - self.ideal = struct.unpack('>L', packet)[0] - self.g, self.p = self.factory.getDHPrime(self.ideal) - self.sendPacket(MSG_KEX_DH_GEX_GROUP, MP(self.p)+MP(self.g)) + self.dhGexRequest = packet + ideal = struct.unpack('>L', packet)[0] + self.g, self.p = self.factory.getDHPrime(ideal) + self.sendPacket(MSG_KEX_DH_GEX_GROUP, MP(self.p) + MP(self.g)) else: - raise error.ConchError('bad kexalg: %s'%self.kexAlg) + raise error.ConchError('bad kexalg: %s' % self.kexAlg) + def ssh_KEX_DH_GEX_REQUEST(self, packet): + """ + Called when we receive a MSG_KEX_DH_GEX_REQUEST message. Payload:: + integer minimum + integer ideal + integer maximum + + The client is asking for a Diffie-Hellman group between minimum and + maximum size, and close to ideal if possible. We reply with a + MSG_KEX_DH_GEX_GROUP message. + + If we were told to ignore the next key exchange packekt by + ssh_KEXINIT, drop it on the floor and return. + """ if self.ignoreNextPacket: self.ignoreNextPacket = 0 return - self.min, self.ideal, self.max = struct.unpack('>3L', packet) - self.g, self.p = self.factory.getDHPrime(self.ideal) - self.sendPacket(MSG_KEX_DH_GEX_GROUP, MP(self.p)+MP(self.g)) + self.dhGexRequest = packet + min, ideal, max = struct.unpack('>3L', packet) + self.g, self.p = self.factory.getDHPrime(ideal) + self.sendPacket(MSG_KEX_DH_GEX_GROUP, MP(self.p) + MP(self.g)) - def ssh_KEX_DH_GEX_INIT(self, packet): - clientDHPubKey, foo = getMP(packet) - # if y < 1024, openssh will reject us: "bad server public DH value". - # y<1024 means f will be short, and of the form 2^y, so an observer - # could trivially derive our secret y from f. Openssh detects this - # and complains, so avoid creating such values by requiring y to be - # larger than ln2(self.p) + def ssh_KEX_DH_GEX_INIT(self, packet): + """ + Called when we get a MSG_KEX_DH_GEX_INIT message. Payload:: + integer e (client DH public key) + We send the MSG_KEX_DH_GEX_REPLY message with our host key and + signature. + """ + clientDHpublicKey, foo = getMP(packet) # TODO: we should also look at the value they send to us and reject # insecure values of f (if g==2 and f has a single '1' bit while the # rest are '0's, then they must have used a small y also). @@ -379,223 +787,300 @@ # TODO: This could be computed when self.p is set up # or do as openssh does and scan f for a single '1' bit instead - minimum = long(math.floor(math.log(self.p) / math.log(2)) + 1) - tries = 0 pSize = Util.number.size(self.p) - y = Util.number.getRandomNumber(pSize, entropy.get_bytes) - while tries < 10 and y < minimum: - tries += 1 - y = Util.number.getRandomNumber(pSize, entropy.get_bytes) - assert(y >= minimum) # TODO: test_conch just hangs if this is hit - # the chance of it being hit are really really low + y = Util.number.getRandomNumber(pSize, randbytes.secureRandom) - f = pow(self.g, y, self.p) - sharedSecret = _MPpow(clientDHPubKey, y, self.p) + serverDHpublicKey = _MPpow(self.g, y, self.p) + sharedSecret = _MPpow(clientDHpublicKey, y, self.p) h = sha.new() h.update(NS(self.otherVersionString)) h.update(NS(self.ourVersionString)) - h.update(NS(self.clientKexInitPayload)) + h.update(NS(self.otherKexInitPayload)) h.update(NS(self.ourKexInitPayload)) - h.update(NS(self.factory.publicKeys[self.keyAlg])) - if self.kexAlg == 'diffie-hellman-group-exchange-sha1': - h.update(struct.pack('>3L', self.min, self.ideal, self.max)) - else: - h.update(struct.pack('>L', self.ideal)) + h.update(NS(self.factory.publicKeys[self.keyAlg].blob())) + h.update(self.dhGexRequest) h.update(MP(self.p)) h.update(MP(self.g)) - h.update(MP(clientDHPubKey)) - h.update(MP(f)) + h.update(MP(clientDHpublicKey)) + h.update(serverDHpublicKey) h.update(sharedSecret) exchangeHash = h.digest() - self.sendPacket(MSG_KEX_DH_GEX_REPLY, NS(self.factory.publicKeys[self.keyAlg])+ \ - MP(f)+NS(keys.signData(self.factory.privateKeys[self.keyAlg], exchangeHash))) + self.sendPacket( + MSG_KEX_DH_GEX_REPLY, + NS(self.factory.publicKeys[self.keyAlg].blob()) + + serverDHpublicKey + + NS(self.factory.privateKeys[self.keyAlg].sign(exchangeHash))) self._keySetup(sharedSecret, exchangeHash) + def ssh_NEWKEYS(self, packet): + """ + Called when we get a MSG_NEWKEYS message. No payload. + When we get this, the keys have been set on both sides, and we + start using them to encrypt and authenticate the connection. + """ + log.msg('NEW KEYS') if packet != '': - self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, "NEWKEYS takes no data") + self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, + "NEWKEYS takes no data") + return self.currentEncryptions = self.nextEncryptions if self.outgoingCompressionType == 'zlib': self.outgoingCompression = zlib.compressobj(6) - #self.outgoingCompression.compress = lambda x: self.outgoingCompression.compress(x) + self.outgoingCompression.flush(zlib.Z_SYNC_FLUSH) if self.incomingCompressionType == 'zlib': self.incomingCompression = zlib.decompressobj() + def ssh_SERVICE_REQUEST(self, packet): + """ + Called when we get a MSG_SERVICE_REQUEST message. Payload:: + string serviceName + + The client has requested a service. If we can start the service, + start it; otherwise, disconnect with + DISCONNECT_SERVICE_NOT_AVAILABLE. + """ service, rest = getNS(packet) cls = self.factory.getService(self, service) if not cls: - self.sendDisconnect(DISCONNECT_SERVICE_NOT_AVAILABLE, "don't have service %s"%service) + self.sendDisconnect(DISCONNECT_SERVICE_NOT_AVAILABLE, + "don't have service %s" % service) return else: self.sendPacket(MSG_SERVICE_ACCEPT, NS(service)) self.setService(cls()) - def _keySetup(self, sharedSecret, exchangeHash): - if not self.sessionID: - self.sessionID = exchangeHash - initIVCS = self._getKey('A', sharedSecret, exchangeHash) - initIVSC = self._getKey('B', sharedSecret, exchangeHash) - encKeyCS = self._getKey('C', sharedSecret, exchangeHash) - encKeySC = self._getKey('D', sharedSecret, exchangeHash) - integKeyCS = self._getKey('E', sharedSecret, exchangeHash) - integKeySC = self._getKey('F', sharedSecret, exchangeHash) - self.nextEncryptions.setKeys(initIVSC, encKeySC, initIVCS, encKeyCS, integKeySC, integKeyCS) - self.sendPacket(MSG_NEWKEYS, '') - def _getKey(self, c, sharedSecret, exchangeHash): - k1 = sha.new(sharedSecret+exchangeHash+c+self.sessionID).digest() - k2 = sha.new(sharedSecret+exchangeHash+k1).digest() - return k1+k2 class SSHClientTransport(SSHTransportBase): - isClient = 1 + """ + SSHClientTransport implements the client side of the SSH protocol. + + @ivar isClient: since we are always the client, this is always True. + + @ivar _gotNewKeys: if we receive a MSG_NEWKEYS message before we are + ready to transition to the new keys, this is set to True so we + can transition when the keys are ready locally. + + @ivar x: our Diffie-Hellman private key. + + @ivar e: our Diffie-Hellman public key. + + @ivar g: the Diffie-Hellman group generator. + + @ivar p: the Diffie-Hellman group prime + + @ivar instance: the SSHService object we are requesting. + """ + isClient = True + def connectionMade(self): + """ + Called when the connection is started with the server. Just sets + up a private instance variable. + """ SSHTransportBase.connectionMade(self) self._gotNewKeys = 0 - def ssh_KEXINIT(self, packet): - self.serverKexInitPayload = chr(MSG_KEXINIT)+packet - #cookie = packet[: 16] # taking this is unimportant - k = getNS(packet[16:], 10) - strings, rest = k[:-1], k[-1] - kexAlgs, keyAlgs, encCS, encSC, macCS, macSC, compCS, compSC, langCS, langSC = \ - [s.split(',')for s in strings] - self.kexAlg = ffs(self.supportedKeyExchanges, kexAlgs) - self.keyAlg = ffs(self.supportedPublicKeys, keyAlgs) - self.nextEncryptions = SSHCiphers( - ffs(self.supportedCiphers, encCS), - ffs(self.supportedCiphers, encSC), - ffs(self.supportedMACs, macCS), - ffs(self.supportedMACs, macSC), - ) - self.outgoingCompressionType = ffs(self.supportedCompressions, compCS) - self.incomingCompressionType = ffs(self.supportedCompressions, compSC) - if None in(self.kexAlg, self.keyAlg, self.outgoingCompressionType, self.incomingCompressionType): - self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, "couldn't match all kex parts") - return - if None in self.nextEncryptions.__dict__.values(): - self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, "couldn't match all kex parts") - return - log.msg('kex alg, key alg: %s %s'%(self.kexAlg, self.keyAlg)) - log.msg('client->server: %s %s %s'%(self.nextEncryptions.outCipType, - self.nextEncryptions.outMacType, - self.outgoingCompressionType)) - log.msg('server->client: %s %s %s'%(self.nextEncryptions.inCipType, - self.nextEncryptions.inMacType, - self.incomingCompressionType)) + def ssh_KEXINIT(self, packet): + """ + Called when we receive a MSG_KEXINIT message. For a description + of the packet, see SSHTransportBase.ssh_KEXINIT(). Additionally, + this method sends the first key exchange packet. If the agreed-upon + exchange is diffie-hellman-group1-sha1, generate a public key + and send it in a MSG_KEXDH_INIT message. If the exchange is + diffie-hellman-group-exchange-sha1, ask for a 2048 bit group with a + MSG_KEX_DH_GEX_REQUEST_OLD message. + """ + if SSHTransportBase.ssh_KEXINIT(self, packet) is None: + return # we disconnected if self.kexAlg == 'diffie-hellman-group1-sha1': - self.x = Util.number.getRandomNumber(512, entropy.get_bytes) - self.DHpubKey = pow(DH_GENERATOR, self.x, DH_PRIME) - self.sendPacket(MSG_KEXDH_INIT, MP(self.DHpubKey)) - else: + self.x = Util.number.getRandomNumber(512, randbytes.secureRandom) + self.e = _MPpow(DH_GENERATOR, self.x, DH_PRIME) + self.sendPacket(MSG_KEXDH_INIT, self.e) + elif self.kexAlg == 'diffie-hellman-group-exchange-sha1': self.sendPacket(MSG_KEX_DH_GEX_REQUEST_OLD, '\x00\x00\x08\x00') + else: + raise error.ConchError("somehow, the kexAlg has been set " + "to something we don't support") + def ssh_KEX_DH_GEX_GROUP(self, packet): + """ + This handles two different message which share an integer value. + If the key exchange is diffie-hellman-group1-sha1, this is + MSG_KEXDH_REPLY. Payload:: + string serverHostKey + integer f (server Diffie-Hellman public key) + string signature + + We verify the host key by calling verifyHostKey, then continue in + _continueKEXDH_REPLY. + + If the key exchange is diffie-hellman-group-exchange-sha1, this is + MSG_KEX_DH_GEX_GROUP. Payload:: + string g (group generator) + string p (group prime) + + We generate a Diffie-Hellman public key and send it in a + MSG_KEX_DH_GEX_INIT message. + """ if self.kexAlg == 'diffie-hellman-group1-sha1': + # actually MSG_KEXDH_REPLY pubKey, packet = getNS(packet) f, packet = getMP(packet) signature, packet = getNS(packet) - fingerprint = ':'.join(map(lambda c: '%02x'%ord(c), md5.new(pubKey).digest())) + fingerprint = ':'.join([ch.encode('hex') for ch in + md5.new(pubKey).digest()]) d = self.verifyHostKey(pubKey, fingerprint) - d.addCallback(self._continueGEX_GROUP, pubKey, f, signature) - d.addErrback(lambda unused,self=self:self.sendDisconnect(DISCONNECT_HOST_KEY_NOT_VERIFIABLE, 'bad host key')) + d.addCallback(self._continueKEXDH_REPLY, pubKey, f, signature) + d.addErrback( + lambda unused: self.sendDisconnect( + DISCONNECT_HOST_KEY_NOT_VERIFIABLE, 'bad host key')) + return d else: self.p, rest = getMP(packet) self.g, rest = getMP(rest) - self.x = getMP('\x00\x00\x00\x40'+entropy.get_bytes(64))[0] - self.DHpubKey = pow(self.g, self.x, self.p) - self.sendPacket(MSG_KEX_DH_GEX_INIT, MP(self.DHpubKey)) + self.x = Util.number.getRandomNumber(320, randbytes.secureRandom) + self.e = _MPpow(self.g, self.x, self.p) + self.sendPacket(MSG_KEX_DH_GEX_INIT, self.e) - def _continueGEX_GROUP(self, ignored, pubKey, f, signature): - serverKey = keys.getPublicKeyObject(pubKey) + + def _continueKEXDH_REPLY(self, ignored, pubKey, f, signature): + """ + The host key has been verified, so we generate the keys. + + @param pubKey: the public key blob for the server's public key. + @type pubKey: C{str} + @param f: the server's Diffie-Hellman public key. + @type f: C{long} + @param signature: the server's signature, verifying that it has the + correct private key. + @type signature: C{str} + """ + serverKey = keys.Key.fromString(pubKey) sharedSecret = _MPpow(f, self.x, DH_PRIME) h = sha.new() h.update(NS(self.ourVersionString)) h.update(NS(self.otherVersionString)) h.update(NS(self.ourKexInitPayload)) - h.update(NS(self.serverKexInitPayload)) + h.update(NS(self.otherKexInitPayload)) h.update(NS(pubKey)) - h.update(MP(self.DHpubKey)) + h.update(self.e) h.update(MP(f)) h.update(sharedSecret) exchangeHash = h.digest() - if not keys.verifySignature(serverKey, signature, exchangeHash): - self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, 'bad signature') + if not serverKey.verify(signature, exchangeHash): + self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, + 'bad signature') return self._keySetup(sharedSecret, exchangeHash) + def ssh_KEX_DH_GEX_REPLY(self, packet): + """ + Called when we receieve a MSG_KEX_DH_GEX_REPLY message. Payload:: + string server host key + integer f (server DH public key) + + We verify the host key by calling verifyHostKey, then continue in + _continueGEX_REPLY. + """ pubKey, packet = getNS(packet) f, packet = getMP(packet) signature, packet = getNS(packet) - fingerprint = ':'.join(map(lambda c: '%02x'%ord(c), md5.new(pubKey).digest())) + fingerprint = ':'.join(map(lambda c: '%02x'%ord(c), + md5.new(pubKey).digest())) d = self.verifyHostKey(pubKey, fingerprint) d.addCallback(self._continueGEX_REPLY, pubKey, f, signature) - d.addErrback(lambda unused, self=self: self.sendDisconnect(DISCONNECT_HOST_KEY_NOT_VERIFIABLE, 'bad host key')) + d.addErrback( + lambda unused: self.sendDisconnect( + DISCONNECT_HOST_KEY_NOT_VERIFIABLE, 'bad host key')) + return d + def _continueGEX_REPLY(self, ignored, pubKey, f, signature): - serverKey = keys.getPublicKeyObject(pubKey) + """ + The host key has been verified, so we generate the keys. + + @param pubKey: the public key blob for the server's public key. + @type pubKey: C{str} + @param f: the server's Diffie-Hellman public key. + @type f: C{long} + @param signature: the server's signature, verifying that it has the + correct private key. + @type signature: C{str} + """ + serverKey = keys.Key.fromString(pubKey) sharedSecret = _MPpow(f, self.x, self.p) h = sha.new() h.update(NS(self.ourVersionString)) h.update(NS(self.otherVersionString)) h.update(NS(self.ourKexInitPayload)) - h.update(NS(self.serverKexInitPayload)) + h.update(NS(self.otherKexInitPayload)) h.update(NS(pubKey)) h.update('\x00\x00\x08\x00') h.update(MP(self.p)) h.update(MP(self.g)) - h.update(MP(self.DHpubKey)) + h.update(self.e) h.update(MP(f)) h.update(sharedSecret) exchangeHash = h.digest() - if not keys.verifySignature(serverKey, signature, exchangeHash): - self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, 'bad signature') + if not serverKey.verify(signature, exchangeHash): + self.sendDisconnect(DISCONNECT_KEY_EXCHANGE_FAILED, + 'bad signature') return self._keySetup(sharedSecret, exchangeHash) + def _keySetup(self, sharedSecret, exchangeHash): - if not self.sessionID: - self.sessionID = exchangeHash - initIVCS = self._getKey('A', sharedSecret, exchangeHash) - initIVSC = self._getKey('B', sharedSecret, exchangeHash) - encKeyCS = self._getKey('C', sharedSecret, exchangeHash) - encKeySC = self._getKey('D', sharedSecret, exchangeHash) - integKeyCS = self._getKey('E', sharedSecret, exchangeHash) - integKeySC = self._getKey('F', sharedSecret, exchangeHash) - self.nextEncryptions.setKeys(initIVCS, encKeyCS, initIVSC, encKeySC, integKeyCS, integKeySC) - self.sendPacket(MSG_NEWKEYS, '') + """ + See SSHTransportBase._keySetup(). + """ + SSHTransportBase._keySetup(self, sharedSecret, exchangeHash) if self._gotNewKeys: self.ssh_NEWKEYS('') - def _getKey(self, c, sharedSecret, exchangeHash): - k1 = sha.new(sharedSecret+exchangeHash+c+self.sessionID).digest() - k2 = sha.new(sharedSecret+exchangeHash+k1).digest() - return k1+k2 def ssh_NEWKEYS(self, packet): + """ + Called when we receieve a MSG_NEWKEYS message. No payload. + If we've finished setting up our own keys, start using them. + Otherwise, remeber that we've receieved this message. + """ if packet != '': - self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, "NEWKEYS takes no data") - if not self.nextEncryptions.enc_block_size: + self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, + "NEWKEYS takes no data") + return + if not self.nextEncryptions.encBlockSize: self._gotNewKeys = 1 return + log.msg('NEW KEYS') self.currentEncryptions = self.nextEncryptions if self.outgoingCompressionType == 'zlib': self.outgoingCompression = zlib.compressobj(6) - #self.outgoingCompression.compress = lambda x: self.outgoingCompression.compress(x) + self.outgoingCompression.flush(zlib.Z_SYNC_FLUSH) if self.incomingCompressionType == 'zlib': self.incomingCompression = zlib.decompressobj() self.connectionSecure() + def ssh_SERVICE_ACCEPT(self, packet): + """ + Called when we receieve a MSG_SERVICE_ACCEPT message. Payload:: + string service name + + Start the service we requested. + """ name = getNS(packet)[0] if name != self.instance.name: - self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, "received accept for service we did not request") + self.sendDisconnect( + DISCONNECT_PROTOCOL_ERROR, + "received accept for service we did not request") self.setService(self.instance) + def requestService(self, instance): """ Request that a service be run over this transport. @@ -605,133 +1090,242 @@ self.sendPacket(MSG_SERVICE_REQUEST, NS(instance.name)) self.instance = instance + # client methods def verifyHostKey(self, hostKey, fingerprint): - """Returns a Deferred that gets a callback if it is a valid key, or + """ + Returns a Deferred that gets a callback if it is a valid key, or an errback if not. @type hostKey: C{str} @type fingerprint: C{str} - @rtype: L{Deferred} + @rtype: L{twisted.internet.defer.Deferred} """ - # return if it's good - return defer.fail(NotImplementedError) + # return if it's good + return defer.fail(NotImplementedError()) + def connectionSecure(self): """ - Called when the encryption has been set up. Generally, + Called when the encryption has been set up. Generally, requestService() is called to run another service over the transport. """ - raise NotImplementedError + raise NotImplementedError() + + class _DummyCipher: - block_size = 1 - + """ + A cipher for the none encryption method. + + @ivar block_size: the block size of the encryption. In the case of the + none cipher, this is 8 bytes. + """ + block_size = 8 + + def encrypt(self, x): return x - + + decrypt = encrypt + class SSHCiphers: + """ + SSHCiphers represents all the encryption operations that need to occur + to encrypt and authenticate the SSH connection. + + @cvar cipherMap: A dictionary mapping SSH encryption names to 3-tuples of + (, , ) + @cvar macMap: A dictionary mapping SSH MAC names to hash modules. + + @ivar outCipType: the string type of the outgoing cipher. + @ivar inCipType: the string type of the incoming cipher. + @ivar outMACType: the string type of the incoming MAC. + @ivar inMACType: the string type of the incoming MAC. + @ivar encBlockSize: the block size of the outgoing cipher. + @ivar decBlockSize: the block size of the incoming cipher. + @ivar verifyDigestSize: the size of the incoming MAC. + @ivar outMAC: a tuple of (, , , + ) representing the outgoing MAC. + @ivar inMAc: see outMAC, but for the incoming MAC. + """ + + cipherMap = { - '3des-cbc':('DES3', 24, 0), - 'blowfish-cbc':('Blowfish', 16,0 ), - 'aes256-cbc':('AES', 32, 0), - 'aes192-cbc':('AES', 24, 0), - 'aes128-cbc':('AES', 16, 0), - 'arcfour':('ARC4', 16, 0), - 'idea-cbc':('IDEA', 16, 0), - 'cast128-cbc':('CAST', 16, 0), + '3des-cbc':('DES3', 24, 0), + 'blowfish-cbc':('Blowfish', 16,0 ), + 'aes256-cbc':('AES', 32, 0), + 'aes192-cbc':('AES', 24, 0), + 'aes128-cbc':('AES', 16, 0), + 'cast128-cbc':('CAST', 16, 0), 'aes128-ctr':('AES', 16, 1), 'aes192-ctr':('AES', 24, 1), 'aes256-ctr':('AES', 32, 1), '3des-ctr':('DES3', 24, 1), 'blowfish-ctr':('Blowfish', 16, 1), - 'idea-ctr':('IDEA', 16, 1), 'cast128-ctr':('CAST', 16, 1), 'none':(None, 0, 0), } macMap = { - 'hmac-sha1': 'sha', - 'hmac-md5': 'md5', + 'hmac-sha1': sha, + 'hmac-md5': md5, 'none':None } + def __init__(self, outCip, inCip, outMac, inMac): self.outCipType = outCip self.inCipType = inCip - self.outMacType = outMac - self.inMacType = inMac - self.enc_block_size = 0 - self.dec_block_size = 0 + self.outMACType = outMac + self.inMACType = inMac + self.encBlockSize = 0 + self.decBlockSize = 0 + self.verifyDigestSize = 0 + self.outMAC = (None, '', '', 0) + self.inMAC = (None, '', '', 0) + def setKeys(self, outIV, outKey, inIV, inKey, outInteg, inInteg): + """ + Set up the ciphers and hashes using the given keys, + + @param outIV: the outgoing initialization vector + @param outKey: the outgoing encryption key + @param inIV: the incoming initialization vector + @param inKey: the incoming encryption key + @param outInteg: the outgoing integrity key + @param inInteg: the incoming integrity key. + """ o = self._getCipher(self.outCipType, outIV, outKey) self.encrypt = o.encrypt - self.enc_block_size = o.block_size + self.encBlockSize = o.block_size o = self._getCipher(self.inCipType, inIV, inKey) self.decrypt = o.decrypt - self.dec_block_size = o.block_size - self.outMAC = self._getMAC(self.outMacType, outInteg) - self.inMAC = self._getMAC(self.inMacType, inInteg) - self.verify_digest_size = self.inMAC[3] + self.decBlockSize = o.block_size + self.outMAC = self._getMAC(self.outMACType, outInteg) + self.inMAC = self._getMAC(self.inMACType, inInteg) + if self.inMAC: + self.verifyDigestSize = self.inMAC[3] + def _getCipher(self, cip, iv, key): + """ + Creates an initialized cipher object. + + @param cip: the name of the cipher: maps into Crypto.Cipher.* + @param iv: the initialzation vector + @param key: the encryption key + """ modName, keySize, counterMode = self.cipherMap[cip] if not modName: # no cipher return _DummyCipher() mod = __import__('Crypto.Cipher.%s'%modName, {}, {}, 'x') if counterMode: - return mod.new(key[:keySize], mod.MODE_CTR, iv[:mod.block_size], counter=_Counter(iv, mod.block_size)) + return mod.new(key[:keySize], mod.MODE_CTR, iv[:mod.block_size], + counter=_Counter(iv, mod.block_size)) else: - return mod.new(key[: keySize], mod.MODE_CBC, iv[: mod.block_size]) + return mod.new(key[:keySize], mod.MODE_CBC, iv[:mod.block_size]) + def _getMAC(self, mac, key): - modName = self.macMap[mac] - if not modName: - return None - mod = __import__(modName, {}, {}, '') - if not hasattr(mod, 'digest_size'): - ds = len(mod.new().digest()) - else: - ds = mod.digest_size - key = key[: ds]+'\x00'*(64-ds) + """ + Gets a 4-tuple representing the message authentication code. + (, , , + ) + + @param mac: a key mapping into macMap + @type mac: C{str} + @param key: the MAC key. + @type key: C{str} + """ + mod = self.macMap[mac] + if not mod: + return (None, '', '', 0) + #if not hasattr(mod, 'digest_size'): + # ds = len(mod.new().digest()) + #else: + ds = mod.digest_size + key = key[:ds] + '\x00' * (64 - ds) i = XOR.new('\x36').encrypt(key) o = XOR.new('\x5c').encrypt(key) - return mod, i,o, ds + return mod, i, o, ds + def encrypt(self, blocks): - return blocks + """ + Encrypt blocks. Overridden by the encrypt method of a + Crypto.Cipher.* object in setKeys(). + + @type blocks: C{str} + """ + raise NotImplementedError() + def decrypt(self, blocks): - return blocks + """ + Decrypt blocks. See encrypt(). + + @type blocks: C{str} + """ + raise NotImplementedError() + def makeMAC(self, seqid, data): - if not self.outMAC: return '' - data = struct.pack('>L', seqid)+data + """ + Create a message authentication code (MAC) for the given packet using + the outgoing MAC values. + + @param seqid: the sequence ID of the outgoing packet + @type seqid: C{int} + @param data: the data to create a MAC for + @type data: C{str} + @rtype: C{str} + """ + if not self.outMAC[0]: + return '' + data = struct.pack('>L', seqid) + data mod, i, o, ds = self.outMAC - inner = mod.new(i+data) - outer = mod.new(o+inner.digest()) + inner = mod.new(i + data) + outer = mod.new(o + inner.digest()) return outer.digest() + def verify(self, seqid, data, mac): - if not self.inMAC: + """ + Verify an incoming MAC using the incoming MAC values. Return True + if the MAC is valid. + + @param seqid: the sequence ID of the incoming packet + @type seqid: C{int} + @param data: the packet data to verify + @type data: C{str} + @param mac: the MAC sent with the packet + @type mac: C{str} + @rtype: C{bool} + """ + if not self.inMAC[0]: return mac == '' - data = struct.pack('>L', seqid)+data - mod, i,o, ds = self.inMAC - inner = mod.new(i+data) - outer = mod.new(o+inner.digest()) + data = struct.pack('>L', seqid) + data + mod, i, o, ds = self.inMAC + inner = mod.new(i + data) + outer = mod.new(o + inner.digest()) return mac == outer.digest() + + class _Counter: """ Stateful counter which returns results packed in a byte string """ + + def __init__(self, initialVector, blockSize): """ @type initialVector: C{str} - @param initialVector: A byte string representing the initial counter value. - + @param initialVector: A byte string representing the initial counter + value. @type blockSize: C{int} @param blockSize: The length of the output buffer, as well as the number of bytes at the beginning of C{initialVector} to consider. @@ -762,22 +1356,16 @@ -def buffer_dump(b, title = None): - r = title or '' - while b: - c, b = b[: 16], b[16:] - while c: - a, c = c[: 2], c[2:] - if len(a) == 2: - r = r+'%02x%02x '%(ord(a[0]), ord(a[1])) - else: - r = r+'%02x'%ord(a[0]) - r = r+'\n' - return r - -DH_PRIME = 179769313486231590770839156793787453197860296048756011706444423684197180216158519368947833795864925541502180565485980503646440548199239100050792877003355816639229553136239076508735759914822574862575007425302077447712589550957937778424442426617334727629299387668709205606050270810842907692932019128194467627007L +# Diffie-Hellman primes from Oakley Group 2 [RFC 2409] +DH_PRIME = long('17976931348623159077083915679378745319786029604875601170644' +'442368419718021615851936894783379586492554150218056548598050364644054819923' +'910005079287700335581663922955313623907650873575991482257486257500742530207' +'744771258955095793777842444242661733472762929938766870920560605027081084290' +'7692932019128194467627007L') DH_GENERATOR = 2L + + MSG_DISCONNECT = 1 MSG_IGNORE = 2 MSG_UNIMPLEMENTED = 3 @@ -794,6 +1382,8 @@ MSG_KEX_DH_GEX_INIT = 32 MSG_KEX_DH_GEX_REPLY = 33 + + DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT = 1 DISCONNECT_PROTOCOL_ERROR = 2 DISCONNECT_KEY_EXCHANGE_FAILED = 3 @@ -810,8 +1400,9 @@ DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 14 DISCONNECT_ILLEGAL_USER_NAME = 15 + + messages = {} for name, value in globals().items(): if name.startswith('MSG_'): messages[value] = name - diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ssh/userauth.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ssh/userauth.py --- twisted-conch-0.8.0/twisted/conch/ssh/userauth.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ssh/userauth.py 2008-07-29 21:13:54.000000000 +0100 @@ -3,12 +3,11 @@ # -"""Implementation of the ssh-userauth service. +""" +Implementation of the ssh-userauth service. Currently implemented authentication types are public-key and password. -This module is unstable. - -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ import struct @@ -87,25 +86,27 @@ self.method = method d = self.tryAuth(method, user, rest) if not d: - self._ebBadAuth(ConchError('auth returned none')) + self._ebBadAuth( + failure.Failure(error.ConchError('auth returned none'))) + return d.addCallbacks(self._cbFinishedAuth) d.addErrback(self._ebMaybeBadAuth) d.addErrback(self._ebBadAuth) + return d def _cbFinishedAuth(self, (interface, avatar, logout)): - self.transport.isAuthorized = True - service = self.transport.factory.getService(self.transport, + self.transport.avatar = avatar + self.transport.logoutFunction = logout + service = self.transport.factory.getService(self.transport, self.nextService) if not service: - raise error.ConchError('could not get next service: %s' + raise error.ConchError('could not get next service: %s' % self.nextService) log.msg('%s authenticated with %s' % (self.user, self.method)) if self.cancelLoginTimeout: self.cancelLoginTimeout.cancel() self.cancelLoginTimeout = None self.transport.sendPacket(MSG_USERAUTH_SUCCESS, '') - self.transport.avatar = avatar - self.transport.logoutFunction = logout self.transport.setService(service()) def _ebMaybeBadAuth(self, reason): diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/stdio.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/stdio.py --- twisted-conch-0.8.0/twisted/conch/stdio.py 2005-07-16 02:55:15.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/stdio.py 2008-07-29 21:13:54.000000000 +0100 @@ -1,12 +1,11 @@ # -*- test-case-name: twisted.conch.test.test_manhole -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2007 Twisted Matrix Laboratories. # See LICENSE for details. -"""Asynchronous local terminal input handling - -API Stability: Unstable +""" +Asynchronous local terminal input handling -@author: U{Jp Calderone} +@author: Jp Calderone """ import os, tty, sys, termios @@ -51,11 +50,21 @@ self.proto.connectionLost(reason) self.proto = None + + class ConsoleManhole(ColoredManhole): - def handle_QUIT(self): - ColoredManhole.handle_QUIT(self) + """ + A manhole protocol specifically for use with L{stdio.StandardIO}. + """ + def connectionLost(self, reason): + """ + When the connection is lost, there is nothing more to do. Stop the + reactor so that the process can exit. + """ reactor.stop() + + def runWithProtocol(klass): fd = sys.__stdin__.fileno() oldSettings = termios.tcgetattr(fd) @@ -66,7 +75,9 @@ reactor.run() finally: termios.tcsetattr(fd, termios.TCSANOW, oldSettings) - os.write(0, "\r\x1bc\r") + os.write(fd, "\r\x1bc\r") + + def main(argv=None): log.startLogging(file('child.log', 'w')) @@ -79,5 +90,6 @@ klass = ConsoleManhole runWithProtocol(klass) + if __name__ == '__main__': main() diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/tap.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/tap.py --- twisted-conch-0.8.0/twisted/conch/tap.py 2005-06-19 23:36:17.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/tap.py 2008-10-28 20:12:14.000000000 +0000 @@ -8,7 +8,7 @@ """ -I am a support module for making SSH servers with mktap. +I am a support module for making SSH servers with twistd. """ from twisted.conch import checkers, unix @@ -19,8 +19,8 @@ class Options(usage.Options): - synopsis = "Usage: mktap conch [-i ] [-p ] [-d ] " - longdesc = "Makes a Conch SSH server.." + synopsis = "[-i ] [-p ] [-d ] " + longdesc = "Makes a Conch SSH server." optParameters = [ ["interface", "i", "", "local interface to which we listen"], ["port", "p", "22", "Port on which to listen"], diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/telnet.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/telnet.py --- twisted-conch-0.8.0/twisted/conch/telnet.py 2006-12-29 21:24:23.000000000 +0000 +++ twisted-conch-8.2.0/twisted/conch/telnet.py 2008-07-29 21:13:54.000000000 +0100 @@ -1,18 +1,17 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# -*- test-case-name: twisted.conch.test.test_telnet -*- +# Copyright (c) 2001-2007 Twisted Matrix Laboratories. # See LICENSE for details. -"""Telnet protocol implementation. - -API Stability: Unstable +""" +Telnet protocol implementation. -@author: U{Jp Calderone} +@author: Jp Calderone """ import struct from zope.interface import implements -from twisted.application import internet from twisted.internet import protocol, interfaces as iinternet, defer from twisted.python import log @@ -433,7 +432,8 @@ self._write(IAC + SB + bytes + IAC + SE) def dataReceived(self, data): - # Most grossly inefficient implementation ever + appDataBuffer = [] + for b in data: if self.state == 'data': if b == IAC: @@ -441,16 +441,19 @@ elif b == '\r': self.state = 'newline' else: - self.applicationDataReceived(b) + appDataBuffer.append(b) elif self.state == 'escaped': if b == IAC: - self.applicationDataReceived(b) + appDataBuffer.append(b) self.state = 'data' elif b == SB: self.state = 'subnegotiation' self.commands = [] elif b in (NOP, DM, BRK, IP, AO, AYT, EC, EL, GA): self.state = 'data' + if appDataBuffer: + self.applicationDataReceived(''.join(appDataBuffer)) + del appDataBuffer[:] self.commandReceived(b, None) elif b in (WILL, WONT, DO, DONT): self.state = 'command' @@ -461,15 +464,31 @@ self.state = 'data' command = self.command del self.command + if appDataBuffer: + self.applicationDataReceived(''.join(appDataBuffer)) + del appDataBuffer[:] self.commandReceived(command, b) elif self.state == 'newline': + self.state = 'data' if b == '\n': - self.applicationDataReceived('\n') + appDataBuffer.append('\n') elif b == '\0': - self.applicationDataReceived('\r') + appDataBuffer.append('\r') + elif b == IAC: + # IAC isn't really allowed after \r, according to the + # RFC, but handling it this way is less surprising than + # delivering the IAC to the app as application data. + # The purpose of the restriction is to allow terminals + # to unambiguously interpret the behavior of the CR + # after reading only one more byte. CR LF is supposed + # to mean one thing (cursor to next line, first column), + # CR NUL another (cursor to first column). Absent the + # NUL, it still makes sense to interpret this as CR and + # then apply all the usual interpretation to the IAC. + appDataBuffer.append('\r') + self.state = 'escaped' else: - self.applicationDataReceived('\r' + b) - self.state = 'data' + appDataBuffer.append('\r' + b) elif self.state == 'subnegotiation': if b == IAC: self.state = 'subnegotiation-escaped' @@ -480,6 +499,9 @@ self.state = 'data' commands = self.commands del self.commands + if appDataBuffer: + self.applicationDataReceived(''.join(appDataBuffer)) + del appDataBuffer[:] self.negotiate(commands) else: self.state = 'subnegotiation' @@ -487,6 +509,10 @@ else: raise ValueError("How'd you do this?") + if appDataBuffer: + self.applicationDataReceived(''.join(appDataBuffer)) + + def connectionLost(self, reason): for state in self.options.values(): if state.us.onResult is not None: @@ -662,16 +688,49 @@ ('yes', False): dont_yes_false, ('yes', True): dont_yes_true} def enableLocal(self, option): - return self.protocol.enableLocal(option) + """ + Reject all attempts to enable options. + """ + return False + def enableRemote(self, option): - return self.protocol.enableRemote(option) + """ + Reject all attempts to enable options. + """ + return False + def disableLocal(self, option): - return self.protocol.disableLocal(option) + """ + Signal a programming error by raising an exception. + + L{enableLocal} must return true for the given value of C{option} in + order for this method to be called. If a subclass of L{Telnet} + overrides enableLocal to allow certain options to be enabled, it must + also override disableLocal to disable those options. + + @raise NotImplementedError: Always raised. + """ + raise NotImplementedError( + "Don't know how to disable local telnet option %r" % (option,)) + def disableRemote(self, option): - return self.protocol.disableRemote(option) + """ + Signal a programming error by raising an exception. + + L{enableRemote} must return true for the given value of C{option} in + order for this method to be called. If a subclass of L{Telnet} + overrides enableRemote to allow certain options to be enabled, it must + also override disableRemote tto disable those options. + + @raise NotImplementedError: Always raised. + """ + raise NotImplementedError( + "Don't know how to disable remote telnet option %r" % (option,)) + + class ProtocolTransportMixin: def write(self, bytes): diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/keydata.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/keydata.py --- twisted-conch-0.8.0/twisted/conch/test/keydata.py 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/keydata.py 2008-03-08 18:41:56.000000000 +0000 @@ -0,0 +1,174 @@ +# -*- test-case-name: twisted.conch.test.test_keys -*- +# Copyright (c) 2007-2008 Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Data used by test_keys as well as others. +""" +RSAData = { + 'n':long('1062486685755247411169438309495398947372127791189432809481' + '382072971106157632182084539383569281493520117634129557550415277' + '516685881326038852354459895734875625093273594925884531272867425' + '864910490065695876046999646807138717162833156501L'), + 'e':35L, + 'd':long('6678487739032983727350755088256793383481946116047863373882' + '973030104095847973715959961839578340816412167985957218887914482' + '713602371850869127033494910375212470664166001439410214474266799' + '85974425203903884190893469297150446322896587555L'), + 'q':long('3395694744258061291019136154000709371890447462086362702627' + '9704149412726577280741108645721676968699696898960891593323L'), + 'p':long('3128922844292337321766351031842562691837301298995834258844' + '4720539204069737532863831050930719431498338835415515173887L')} + +DSAData = { + 'y':long('2300663509295750360093768159135720439490120577534296730713' + '348508834878775464483169644934425336771277908527130096489120714' + '610188630979820723924744291603865L'), + 'g':long('4451569990409370769930903934104221766858515498655655091803' + '866645719060300558655677517139568505649468378587802312867198352' + '1161998270001677664063945776405L'), + 'p':long('7067311773048598659694590252855127633397024017439939353776' + '608320410518694001356789646664502838652272205440894335303988504' + '978724817717069039110940675621677L'), + 'q':1184501645189849666738820838619601267690550087703L, + 'x':863951293559205482820041244219051653999559962819L} + +publicRSA_openssh = ("ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEArzJx8OYOnJmzf4tfBE" +"vLi8DVPrJ3/c9k2I/Az64fxjHf9imyRJbixtQhlH9lfNjUIx+4LmrJH5QNRsFporcHDKOTwTTYL" +"h5KmRpslkYHRivcJSkbh/C+BR3utDS555mV comment") + +privateRSA_openssh = """-----BEGIN RSA PRIVATE KEY----- +MIIByAIBAAJhAK8ycfDmDpyZs3+LXwRLy4vA1T6yd/3PZNiPwM+uH8Yx3/YpskSW +4sbUIZR/ZXzY1CMfuC5qyR+UDUbBaaK3Bwyjk8E02C4eSpkabJZGB0Yr3CUpG4fw +vgUd7rQ0ueeZlQIBIwJgbh+1VZfr7WftK5lu7MHtqE1S1vPWZQYE3+VUn8yJADyb +Z4fsZaCrzW9lkIqXkE3GIY+ojdhZhkO1gbG0118sIgphwSWKRxK0mvh6ERxKqIt1 +xJEJO74EykXZV4oNJ8sjAjEA3J9r2ZghVhGN6V8DnQrTk24Td0E8hU8AcP0FVP+8 +PQm/g/aXf2QQkQT+omdHVEJrAjEAy0pL0EBH6EVS98evDCBtQw22OZT52qXlAwZ2 +gyTriKFVoqjeEjt3SZKKqXHSApP/AjBLpF99zcJJZRq2abgYlf9lv1chkrWqDHUu +DZttmYJeEfiFBBavVYIF1dOlZT0G8jMCMBc7sOSZodFnAiryP+Qg9otSBjJ3bQML +pSTqy7c3a2AScC/YyOwkDaICHnnD3XyjMwIxALRzl0tQEKMXs6hH8ToUdlLROCrP +EhQ0wahUTCk1gKA4uPD6TMTChavbh4K63OvbKg== +-----END RSA PRIVATE KEY-----""" + +# some versions of OpenSSH generate these (slightly different keys) +privateRSA_openssh_alternate = """-----BEGIN RSA PRIVATE KEY----- +MIIBzjCCAcgCAQACYQCvMnHw5g6cmbN/i18ES8uLwNU+snf9z2TYj8DPrh/GMd/2 +KbJEluLG1CGUf2V82NQjH7guaskflA1GwWmitwcMo5PBNNguHkqZGmyWRgdGK9wl +KRuH8L4FHe60NLnnmZUCASMCYG4ftVWX6+1n7SuZbuzB7ahNUtbz1mUGBN/lVJ/M +iQA8m2eH7GWgq81vZZCKl5BNxiGPqI3YWYZDtYGxtNdfLCIKYcElikcStJr4ehEc +SqiLdcSRCTu+BMpF2VeKDSfLIwIxANyfa9mYIVYRjelfA50K05NuE3dBPIVPAHD9 +BVT/vD0Jv4P2l39kEJEE/qJnR1RCawIxAMtKS9BAR+hFUvfHrwwgbUMNtjmU+dql +5QMGdoMk64ihVaKo3hI7d0mSiqlx0gKT/wIwS6Rffc3CSWUatmm4GJX/Zb9XIZK1 +qgx1Lg2bbZmCXhH4hQQWr1WCBdXTpWU9BvIzAjAXO7DkmaHRZwIq8j/kIPaLUgYy +d20DC6Uk6su3N2tgEnAv2MjsJA2iAh55w918ozMCMQC0c5dLUBCjF7OoR/E6FHZS +0TgqzxIUNMGoVEwpNYCgOLjw+kzEwoWr24eCutzr2yowAA== +------END RSA PRIVATE KEY------""" + +privateRSA_openssh_encrypted = """-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,FFFFFFFFFFFFFFFF + +30qUR7DYY/rpVJu159paRM1mUqt/IMibfEMTKWSjNhCVD21hskftZCJROw/WgIFt +ncusHpJMkjgwEpho0KyKilcC7zxjpunTex24Meb5pCdXCrYft8AyUkRdq3dugMqT +4nuWuWxziluBhKQ2M9tPGcEOeulU4vVjceZt2pZhZQVBf08o3XUv5/7RYd24M9md +WIo+5zdj2YQkI6xMFTP954O/X32ME1KQt98wgNEy6mxhItbvf00mH3woALwEKP3v +PSMxxtx3VKeDKd9YTOm1giKkXZUf91vZWs0378tUBrU4U5qJxgryTjvvVKOtofj6 +4qQy6+r6M6wtwVlXBgeRm2gBPvL3nv6MsROp3E6ztBd/e7A8fSec+UTq3ko/EbGP +0QG+IG5tg8FsdITxQ9WAIITZL3Rc6hA5Ymx1VNhySp3iSiso8Jof27lku4pyuvRV +ko/B3N2H7LnQrGV0GyrjeYocW/qZh/PCsY48JBFhlNQexn2mn44AJW3y5xgbhvKA +3mrmMD1hD17ZvZxi4fPHjbuAyM1vFqhQx63eT9ijbwJ91svKJl5O5MIv41mCRonm +hxvOXw8S0mjSasyofptzzQCtXxFLQigXbpQBltII+Ys= +-----END RSA PRIVATE KEY-----""" + +publicRSA_lsh = ("{KDEwOnB1YmxpYy1rZXkoMTQ6cnNhLXBrY3MxLXNoYTEoMTpuOTc6AK8yc" +"fDmDpyZs3+LXwRLy4vA1T6yd/3PZNiPwM+uH8Yx3/YpskSW4sbUIZR/ZXzY1CMfuC5qyR+UDUbB" +"aaK3Bwyjk8E02C4eSpkabJZGB0Yr3CUpG4fwvgUd7rQ0ueeZlSkoMTplMTojKSkp}") + +privateRSA_lsh = ("(11:private-key(9:rsa-pkcs1(1:n97:\x00\xaf2q\xf0\xe6\x0e" +"\x9c\x99\xb3\x7f\x8b_\x04K\xcb\x8b\xc0\xd5>\xb2w\xfd\xcfd\xd8\x8f\xc0\xcf" +"\xae\x1f\xc61\xdf\xf6)\xb2D\x96\xe2\xc6\xd4!\x94\x7fe|\xd8\xd4#\x1f\xb8.j" +"\xc9\x1f\x94\rF\xc1i\xa2\xb7\x07\x0c\xa3\x93\xc14\xd8.\x1eJ\x99\x1al\x96F" +"\x07F+\xdc%)\x1b\x87\xf0\xbe\x05\x1d\xee\xb44\xb9\xe7\x99\x95)(1:e1:#)(1:d9" +"6:n\x1f\xb5U\x97\xeb\xedg\xed+\x99n\xec\xc1\xed\xa8MR\xd6\xf3\xd6e\x06\x04" +"\xdf\xe5T\x9f\xcc\x89\x00<\x9bg\x87\xece\xa0\xab\xcdoe\x90\x8a\x97\x90M\xc6" +'!\x8f\xa8\x8d\xd8Y\x86C\xb5\x81\xb1\xb4\xd7_,"\na\xc1%\x8aG\x12\xb4\x9a\xf8' +"z\x11\x1cJ\xa8\x8bu\xc4\x91\t;\xbe\x04\xcaE\xd9W\x8a\r\'\xcb#)(1:p49:\x00" +"\xdc\x9fk\xd9\x98!V\x11\x8d\xe9_\x03\x9d\n\xd3\x93n\x13wA<\x85O\x00p\xfd" +"\x05T\xff\xbc=\t\xbf\x83\xf6\x97\x7fd\x10\x91\x04\xfe\xa2gGTBk)(1:q49:\x00" +"\xcbJK\xd0@G\xe8ER\xf7\xc7\xaf\x0c mC\r\xb69\x94\xf9\xda\xa5\xe5\x03\x06v" +"\x83$\xeb\x88\xa1U\xa2\xa8\xde\x12;wI\x92\x8a\xa9q\xd2\x02\x93\xff)(1:a48:K" +"\xa4_}\xcd\xc2Ie\x1a\xb6i\xb8\x18\x95\xffe\xbfW!\x92\xb5\xaa\x0cu.\r\x9bm" +"\x99\x82^\x11\xf8\x85\x04\x16\xafU\x82\x05\xd5\xd3\xa5e=\x06\xf23)(1:b48:" +"\x17;\xb0\xe4\x99\xa1\xd1g\x02*\xf2?\xe4 \xf6\x8bR\x062wm\x03\x0b\xa5$\xea" +"\xcb\xb77k`\x12p/\xd8\xc8\xec$\r\xa2\x02\x1ey\xc3\xdd|\xa33)(1:c49:\x00\xb4" +"s\x97KP\x10\xa3\x17\xb3\xa8G\xf1:\x14vR\xd18*\xcf\x12\x144\xc1\xa8TL)5\x80" +"\xa08\xb8\xf0\xfaL\xc4\xc2\x85\xab\xdb\x87\x82\xba\xdc\xeb\xdb*)))") + +privateRSA_agentv3 = ("\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01#\x00\x00\x00`" +"n\x1f\xb5U\x97\xeb\xedg\xed+\x99n\xec\xc1\xed\xa8MR\xd6\xf3\xd6e\x06\x04" +"\xdf\xe5T\x9f\xcc\x89\x00<\x9bg\x87\xece\xa0\xab\xcdoe\x90\x8a\x97\x90M\xc6" +'!\x8f\xa8\x8d\xd8Y\x86C\xb5\x81\xb1\xb4\xd7_,"\na\xc1%\x8aG\x12\xb4\x9a\xf8' +"z\x11\x1cJ\xa8\x8bu\xc4\x91\t;\xbe\x04\xcaE\xd9W\x8a\r\'\xcb#\x00\x00\x00a" +"\x00\xaf2q\xf0\xe6\x0e\x9c\x99\xb3\x7f\x8b_\x04K\xcb\x8b\xc0\xd5>\xb2w\xfd" +"\xcfd\xd8\x8f\xc0\xcf\xae\x1f\xc61\xdf\xf6)\xb2D\x96\xe2\xc6\xd4!\x94\x7fe|" +"\xd8\xd4#\x1f\xb8.j\xc9\x1f\x94\rF\xc1i\xa2\xb7\x07\x0c\xa3\x93\xc14\xd8." +"\x1eJ\x99\x1al\x96F\x07F+\xdc%)\x1b\x87\xf0\xbe\x05\x1d\xee\xb44\xb9\xe7" +"\x99\x95\x00\x00\x001\x00\xb4s\x97KP\x10\xa3\x17\xb3\xa8G\xf1:\x14vR\xd18*" +"\xcf\x12\x144\xc1\xa8TL)5\x80\xa08\xb8\xf0\xfaL\xc4\xc2\x85\xab\xdb\x87\x82" +"\xba\xdc\xeb\xdb*\x00\x00\x001\x00\xcbJK\xd0@G\xe8ER\xf7\xc7\xaf\x0c mC\r" +"\xb69\x94\xf9\xda\xa5\xe5\x03\x06v\x83$\xeb\x88\xa1U\xa2\xa8\xde\x12;wI\x92" +"\x8a\xa9q\xd2\x02\x93\xff\x00\x00\x001\x00\xdc\x9fk\xd9\x98!V\x11\x8d\xe9_" +"\x03\x9d\n\xd3\x93n\x13wA<\x85O\x00p\xfd\x05T\xff\xbc=\t\xbf\x83\xf6\x97" +"\x7fd\x10\x91\x04\xfe\xa2gGTBk") + +publicDSA_openssh = ("ssh-dss AAAAB3NzaC1kc3MAAABBAIbwTOSsZ7Bl7U1KyMNqV13Tu7" +"yRAtTr70PVI3QnfrPumf2UzCgpL1ljbKxSfAi05XvrE/1vfCFAsFYXRZLhQy0AAAAVAM965Akmo" +"6eAi7K+k9qDR4TotFAXAAAAQADZlpTW964haQWS4vC063NGdldT6xpUGDcDRqbm90CoPEa2RmNO" +"uOqi8lnbhYraEzypYH3K4Gzv/bxCBnKtHRUAAABAK+1osyWBS0+P90u/rAuko6chZ98thUSY2kL" +"SHp6hLKyy2bjnT29h7haELE+XHfq2bM9fckDx2FLOSIJzy83VmQ== comment") + +privateDSA_openssh = """-----BEGIN DSA PRIVATE KEY----- +MIH4AgEAAkEAhvBM5KxnsGXtTUrIw2pXXdO7vJEC1OvvQ9UjdCd+s+6Z/ZTMKCkv +WWNsrFJ8CLTle+sT/W98IUCwVhdFkuFDLQIVAM965Akmo6eAi7K+k9qDR4TotFAX +AkAA2ZaU1veuIWkFkuLwtOtzRnZXU+saVBg3A0am5vdAqDxGtkZjTrjqovJZ24WK +2hM8qWB9yuBs7/28QgZyrR0VAkAr7WizJYFLT4/3S7+sC6SjpyFn3y2FRJjaQtIe +nqEsrLLZuOdPb2HuFoQsT5cd+rZsz19yQPHYUs5IgnPLzdWZAhUAl1TqdmlAG/b4 +nnVchGiO9sML8MM= +-----END DSA PRIVATE KEY-----""" + +publicDSA_lsh = ("{KDEwOnB1YmxpYy1rZXkoMzpkc2EoMTpwNjU6AIbwTOSsZ7Bl7U1KyMNqV" +"13Tu7yRAtTr70PVI3QnfrPumf2UzCgpL1ljbKxSfAi05XvrE/1vfCFAsFYXRZLhQy0pKDE6cTIx" +"OgDPeuQJJqOngIuyvpPag0eE6LRQFykoMTpnNjQ6ANmWlNb3riFpBZLi8LTrc0Z2V1PrGlQYNwN" +"Gpub3QKg8RrZGY0646qLyWduFitoTPKlgfcrgbO/9vEIGcq0dFSkoMTp5NjQ6K+1osyWBS0+P90" +"u/rAuko6chZ98thUSY2kLSHp6hLKyy2bjnT29h7haELE+XHfq2bM9fckDx2FLOSIJzy83VmSkpK" +"Q==}") + +privateDSA_lsh = ("(11:private-key(3:dsa(1:p65:\x00\x86\xf0L\xe4\xacg\xb0e" +"\xedMJ\xc8\xc3jW]\xd3\xbb\xbc\x91\x02\xd4\xeb\xefC\xd5#t'~\xb3\xee\x99\xfd" +"\x94\xcc()/Ycl\xacR|\x08\xb4\xe5{\xeb\x13\xfdo|!@\xb0V\x17E\x92\xe1C-)(1:q2" +"1:\x00\xcfz\xe4\t&\xa3\xa7\x80\x8b\xb2\xbe\x93\xda\x83G\x84\xe8\xb4P\x17)(1" +":g64:\x00\xd9\x96\x94\xd6\xf7\xae!i\x05\x92\xe2\xf0\xb4\xebsFvWS\xeb\x1aT" +"\x187\x03F\xa6\xe6\xf7@\xa8 server + self.client, self.server, self.pump = iosim.connectedServerAndClient( + agent.SSHAgentServer, agent.SSHAgentClient) + + # the server's end of the protocol is stateful and we store it on the + # factory, for which we only need a mock + self.server.factory = StubFactory() + + # pub/priv keys of each kind + self.rsaPrivate = keys.Key.fromString(keydata.privateRSA_openssh) + self.dsaPrivate = keys.Key.fromString(keydata.privateDSA_openssh) + + self.rsaPublic = keys.Key.fromString(keydata.publicRSA_openssh) + self.dsaPublic = keys.Key.fromString(keydata.publicDSA_openssh) + + + +class TestServerProtocolContractWithFactory(AgentTestBase): + """ + The server protocol is stateful and so uses its factory to track state + across requests. This test asserts that the protocol raises if its factory + doesn't provide the necessary storage for that state. + """ + def test_factorySuppliesKeyStorageForServerProtocol(self): + # need a message to send into the server + msg = struct.pack('!LB',1, agent.AGENTC_REQUEST_IDENTITIES) + del self.server.factory.__dict__['keys'] + self.assertRaises(MissingKeyStoreError, + self.server.dataReceived, msg) + + + +class TestUnimplementedVersionOneServer(AgentTestBase): + """ + Tests for methods with no-op implementations on the server. We need these + for clients, such as openssh, that try v1 methods before going to v2. + + Because the client doesn't expose these operations with nice method names, + we invoke sendRequest directly with an op code. + """ + + def test_agentc_REQUEST_RSA_IDENTITIES(self): + """ + assert that we get the correct op code for an RSA identities request + """ + d = self.client.sendRequest(agent.AGENTC_REQUEST_RSA_IDENTITIES, '') + self.pump.flush() + def _cb(packet): + self.assertEqual( + agent.AGENT_RSA_IDENTITIES_ANSWER, ord(packet[0])) + return d.addCallback(_cb) + + + def test_agentc_REMOVE_RSA_IDENTITY(self): + """ + assert that we get the correct op code for an RSA remove identity request + """ + d = self.client.sendRequest(agent.AGENTC_REMOVE_RSA_IDENTITY, '') + self.pump.flush() + return d.addCallback(self.assertEqual, '') + + + def test_agentc_REMOVE_ALL_RSA_IDENTITIES(self): + """ + assert that we get the correct op code for an RSA remove all identities + request. + """ + d = self.client.sendRequest(agent.AGENTC_REMOVE_ALL_RSA_IDENTITIES, '') + self.pump.flush() + return d.addCallback(self.assertEqual, '') + + + +if agent is not None: + class CorruptServer(agent.SSHAgentServer): + """ + A misbehaving server that returns bogus response op codes so that we can + verify that our callbacks that deal with these op codes handle such + miscreants. + """ + def agentc_REQUEST_IDENTITIES(self, data): + self.sendResponse(254, '') + + + def agentc_SIGN_REQUEST(self, data): + self.sendResponse(254, '') + + + +class TestClientWithBrokenServer(AgentTestBase): + """ + verify error handling code in the client using a misbehaving server + """ + + def setUp(self): + AgentTestBase.setUp(self) + self.client, self.server, self.pump = iosim.connectedServerAndClient( + CorruptServer, agent.SSHAgentClient) + # the server's end of the protocol is stateful and we store it on the + # factory, for which we only need a mock + self.server.factory = StubFactory() + + + def test_signDataCallbackErrorHandling(self): + """ + Assert that L{SSHAgentClient.signData} raises a ConchError + if we get a response from the server whose opcode doesn't match + the protocol for data signing requests. + """ + d = self.client.signData(self.rsaPublic.blob(), "John Hancock") + self.pump.flush() + return self.assertFailure(d, ConchError) + + + def test_requestIdentitiesCallbackErrorHandling(self): + """ + Assert that L{SSHAgentClient.requestIdentities} raises a ConchError + if we get a response from the server whose opcode doesn't match + the protocol for identity requests. + """ + d = self.client.requestIdentities() + self.pump.flush() + return self.assertFailure(d, ConchError) + + + +class TestAgentKeyAddition(AgentTestBase): + """ + Test adding different flavors of keys to an agent. + """ + + def test_addRSAIdentityNoComment(self): + """ + L{SSHAgentClient.addIdentity} adds the private key it is called + with to the SSH agent server to which it is connected, associating + it with the comment it is called with. + + This test asserts that ommitting the comment produces an + empty string for the comment on the server. + """ + d = self.client.addIdentity(self.rsaPrivate.privateBlob()) + self.pump.flush() + def _check(ignored): + serverKey = self.server.factory.keys[self.rsaPrivate.blob()] + self.assertEqual(self.rsaPrivate, serverKey[0]) + self.assertEqual('', serverKey[1]) + return d.addCallback(_check) + + + def test_addDSAIdentityNoComment(self): + """ + L{SSHAgentClient.addIdentity} adds the private key it is called + with to the SSH agent server to which it is connected, associating + it with the comment it is called with. + + This test asserts that ommitting the comment produces an + empty string for the comment on the server. + """ + d = self.client.addIdentity(self.dsaPrivate.privateBlob()) + self.pump.flush() + def _check(ignored): + serverKey = self.server.factory.keys[self.dsaPrivate.blob()] + self.assertEqual(self.dsaPrivate, serverKey[0]) + self.assertEqual('', serverKey[1]) + return d.addCallback(_check) + + + def test_addRSAIdentityWithComment(self): + """ + L{SSHAgentClient.addIdentity} adds the private key it is called + with to the SSH agent server to which it is connected, associating + it with the comment it is called with. + + This test asserts that the server receives/stores the comment + as sent by the client. + """ + d = self.client.addIdentity( + self.rsaPrivate.privateBlob(), comment='My special key') + self.pump.flush() + def _check(ignored): + serverKey = self.server.factory.keys[self.rsaPrivate.blob()] + self.assertEqual(self.rsaPrivate, serverKey[0]) + self.assertEqual('My special key', serverKey[1]) + return d.addCallback(_check) + + + def test_addDSAIdentityWithComment(self): + """ + L{SSHAgentClient.addIdentity} adds the private key it is called + with to the SSH agent server to which it is connected, associating + it with the comment it is called with. + + This test asserts that the server receives/stores the comment + as sent by the client. + """ + d = self.client.addIdentity( + self.dsaPrivate.privateBlob(), comment='My special key') + self.pump.flush() + def _check(ignored): + serverKey = self.server.factory.keys[self.dsaPrivate.blob()] + self.assertEqual(self.dsaPrivate, serverKey[0]) + self.assertEqual('My special key', serverKey[1]) + return d.addCallback(_check) + + + +class TestAgentClientFailure(AgentTestBase): + def test_agentFailure(self): + """ + verify that the client raises ConchError on AGENT_FAILURE + """ + d = self.client.sendRequest(254, '') + self.pump.flush() + return self.assertFailure(d, ConchError) + + + +class TestAgentIdentityRequests(AgentTestBase): + """ + Test operations against a server with identities already loaded. + """ + + def setUp(self): + AgentTestBase.setUp(self) + self.server.factory.keys[self.dsaPrivate.blob()] = ( + self.dsaPrivate, 'a comment') + self.server.factory.keys[self.rsaPrivate.blob()] = ( + self.rsaPrivate, 'another comment') + + + def test_signDataRSA(self): + """ + Sign data with an RSA private key and then verify it with the public + key. + """ + d = self.client.signData(self.rsaPublic.blob(), "John Hancock") + self.pump.flush() + def _check(sig): + expected = self.rsaPrivate.sign("John Hancock") + self.assertEqual(expected, sig) + self.assertTrue(self.rsaPublic.verify(sig, "John Hancock")) + return d.addCallback(_check) + + + def test_signDataDSA(self): + """ + Sign data with a DSA private key and then verify it with the public + key. + """ + d = self.client.signData(self.dsaPublic.blob(), "John Hancock") + self.pump.flush() + def _check(sig): + # Cannot do this b/c DSA uses random numbers when signing + # expected = self.dsaPrivate.sign("John Hancock") + # self.assertEquals(expected, sig) + self.assertTrue(self.dsaPublic.verify(sig, "John Hancock")) + return d.addCallback(_check) + + + def test_signDataRSAErrbackOnUnknownBlob(self): + """ + Assert that we get an errback if we try to sign data using a key that + wasn't added. + """ + del self.server.factory.keys[self.rsaPublic.blob()] + d = self.client.signData(self.rsaPublic.blob(), "John Hancock") + self.pump.flush() + return self.assertFailure(d, ConchError) + + + def test_requestIdentities(self): + """ + Assert that we get all of the keys/comments that we add when we issue a + request for all identities. + """ + d = self.client.requestIdentities() + self.pump.flush() + def _check(keyt): + expected = {} + expected[self.dsaPublic.blob()] = 'a comment' + expected[self.rsaPublic.blob()] = 'another comment' + + received = {} + for k in keyt: + received[keys.Key.fromString(k[0], type='blob').blob()] = k[1] + self.assertEquals(expected, received) + return d.addCallback(_check) + + + +class TestAgentKeyRemoval(AgentTestBase): + """ + Test support for removing keys in a remote server. + """ + + def setUp(self): + AgentTestBase.setUp(self) + self.server.factory.keys[self.dsaPrivate.blob()] = ( + self.dsaPrivate, 'a comment') + self.server.factory.keys[self.rsaPrivate.blob()] = ( + self.rsaPrivate, 'another comment') + + + def test_removeRSAIdentity(self): + """ + Assert that we can remove an RSA identity. + """ + # only need public key for this + d = self.client.removeIdentity(self.rsaPrivate.blob()) + self.pump.flush() + + def _check(ignored): + self.assertEqual(1, len(self.server.factory.keys)) + self.assertIn(self.dsaPrivate.blob(), self.server.factory.keys) + self.assertNotIn(self.rsaPrivate.blob(), self.server.factory.keys) + return d.addCallback(_check) + + + def test_removeDSAIdentity(self): + """ + Assert that we can remove a DSA identity. + """ + # only need public key for this + d = self.client.removeIdentity(self.dsaPrivate.blob()) + self.pump.flush() + + def _check(ignored): + self.assertEqual(1, len(self.server.factory.keys)) + self.assertIn(self.rsaPrivate.blob(), self.server.factory.keys) + return d.addCallback(_check) + + + def test_removeAllIdentities(self): + """ + Assert that we can remove all identities. + """ + d = self.client.removeAllIdentities() + self.pump.flush() + + def _check(ignored): + self.assertEquals(0, len(self.server.factory.keys)) + return d.addCallback(_check) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_cftp.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_cftp.py --- twisted-conch-0.8.0/twisted/conch/test/test_cftp.py 2006-12-05 14:35:31.000000000 +0000 +++ twisted-conch-8.2.0/twisted/conch/test/test_cftp.py 2008-09-10 15:58:45.000000000 +0100 @@ -1,13 +1,19 @@ # -*- test-case-name: twisted.conch.test.test_cftp -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE file for details. -import sys +import sys, os + +try: + import Crypto.Cipher.DES3 +except ImportError: + Crypto = None try: from twisted.conch import unix from twisted.conch.scripts import cftp from twisted.conch.client import connect, default, options + from twisted.conch.test.test_filetransfer import FileTransferForTestAvatar except ImportError: unix = None try: @@ -16,23 +22,17 @@ # In Python 2.4, the bad import has already been cleaned up for us. pass -try: - import Crypto -except ImportError: - Crypto = None - from twisted.cred import portal from twisted.internet import reactor, protocol, interfaces, defer, error from twisted.internet.utils import getProcessOutputAndValue from twisted.python import log -from twisted.test import test_process -import test_ssh, test_conch -from test_filetransfer import SFTPTestBase, FileTransferTestAvatar -import sys, os, time, tempfile +from twisted.conch.test import test_ssh, test_conch +from twisted.conch.test.test_filetransfer import SFTPTestBase +from twisted.conch.test.test_filetransfer import FileTransferTestAvatar -class FileTransferTestRealm: +class FileTransferTestRealm: def __init__(self, testDir): self.testDir = testDir @@ -42,36 +42,139 @@ class SFTPTestProcess(protocol.ProcessProtocol): + """ + Protocol for testing cftp. Provides an interface between Python (where all + the tests are) and the cftp client process (which does the work that is + being tested). + """ - def __init__(self): + def __init__(self, onOutReceived): + """ + @param onOutReceived: A L{Deferred} to be fired as soon as data is + received from stdout. + """ self.clearBuffer() - self.connected = 0 - - def connectionMade(self): - self.connected = 1 + self.onOutReceived = onOutReceived + self.onProcessEnd = None + self._expectingCommand = None + self._processEnded = False def clearBuffer(self): + """ + Clear any buffered data received from stdout. Should be private. + """ self.buffer = '' + self._linesReceived = [] + self._lineBuffer = '' def outReceived(self, data): + """ + Called by Twisted when the cftp client prints data to stdout. + """ log.msg('got %s' % data) + lines = (self._lineBuffer + data).split('\n') + self._lineBuffer = lines.pop(-1) + self._linesReceived.extend(lines) + # XXX - not strictly correct. + # We really want onOutReceived to fire after the first 'cftp>' prompt + # has been received. (See use in TestOurServerCmdLineClient.setUp) + if self.onOutReceived is not None: + d, self.onOutReceived = self.onOutReceived, None + d.callback(data) self.buffer += data + self._checkForCommand() + + def _checkForCommand(self): + prompt = 'cftp> ' + if self._expectingCommand and self._lineBuffer == prompt: + buf = '\n'.join(self._linesReceived) + if buf.startswith(prompt): + buf = buf[len(prompt):] + self.clearBuffer() + d, self._expectingCommand = self._expectingCommand, None + d.callback(buf) def errReceived(self, data): + """ + Called by Twisted when the cftp client prints data to stderr. + """ log.msg('err: %s' % data) - def connectionLost(self, reason): - self.connected = 0 def getBuffer(self): + """ + Return the contents of the buffer of data received from stdout. + """ return self.buffer -class CFTPClientTestBase(SFTPTestBase): + def runCommand(self, command): + """ + Issue the given command via the cftp client. Return a C{Deferred} that + fires when the server returns a result. Note that the C{Deferred} will + callback even if the server returns some kind of error. + + @param command: A string containing an sftp command. + + @return: A C{Deferred} that fires when the sftp server returns a + result. The payload is the server's response string. + """ + self._expectingCommand = defer.Deferred() + self.clearBuffer() + self.transport.write(command + '\n') + return self._expectingCommand + + def runScript(self, commands): + """ + Run each command in sequence and return a Deferred that fires when all + commands are completed. + + @param commands: A list of strings containing sftp commands. + + @return: A C{Deferred} that fires when all commands are completed. The + payload is a list of response strings from the server, in the same + order as the commands. + """ + sem = defer.DeferredSemaphore(1) + dl = [sem.run(self.runCommand, command) for command in commands] + return defer.gatherResults(dl) - def setUpClass(self): - open('dsa_test.pub','w').write(test_ssh.publicDSA_openssh) - open('dsa_test','w').write(test_ssh.privateDSA_openssh) + def killProcess(self): + """ + Kill the process if it is still running. + + If the process is still running, sends a KILL signal to the transport + and returns a C{Deferred} which fires when L{processEnded} is called. + + @return: a C{Deferred}. + """ + if self._processEnded: + return defer.succeed(None) + self.onProcessEnd = defer.Deferred() + self.transport.signalProcess('KILL') + return self.onProcessEnd + + def processEnded(self, reason): + """ + Called by Twisted when the cftp client process ends. + """ + self._processEnded = True + if self.onProcessEnd: + d, self.onProcessEnd = self.onProcessEnd, None + d.callback(None) + + +class CFTPClientTestBase(SFTPTestBase): + def setUp(self): + f = open('dsa_test.pub','w') + f.write(test_ssh.publicDSA_openssh) + f.close() + f = open('dsa_test','w') + f.write(test_ssh.privateDSA_openssh) + f.close() os.chmod('dsa_test', 33152) - open('kh_test','w').write('127.0.0.1 '+test_ssh.publicRSA_openssh) + f = open('kh_test','w') + f.write('127.0.0.1 ' + test_ssh.publicRSA_openssh) + f.close() + return SFTPTestBase.setUp(self) def startServer(self): realm = FileTransferTestRealm(self.testDir) @@ -93,20 +196,17 @@ def _cbStopServer(self, ignored): return defer.maybeDeferred(self.server.stopListening) - def tearDownClass(self): + def tearDown(self): for f in ['dsa_test.pub', 'dsa_test', 'kh_test']: try: os.remove(f) except: pass + return SFTPTestBase.tearDown(self) -class TestOurServerCmdLineClient(test_process.SignalMixin, CFTPClientTestBase): - def setUpClass(self): - if hasattr(self, 'skip'): - return - test_process.SignalMixin.setUpClass(self) - CFTPClientTestBase.setUpClass(self) + +class TestOurServerCmdLineClient(CFTPClientTestBase): def setUp(self): CFTPClientTestBase.setUp(self) @@ -124,30 +224,18 @@ port = self.server.getHost().port cmds = test_conch._makeArgs((cmds % port).split(), mod='cftp') log.msg('running %s %s' % (sys.executable, cmds)) - self.processProtocol = SFTPTestProcess() - + d = defer.Deferred() + self.processProtocol = SFTPTestProcess(d) + d.addCallback(lambda _: self.processProtocol.clearBuffer()) env = os.environ.copy() env['PYTHONPATH'] = os.pathsep.join(sys.path) reactor.spawnProcess(self.processProtocol, sys.executable, cmds, env=env) - - timeout = time.time() + 10 - while (not self.processProtocol.buffer) and (time.time() < timeout): - reactor.iterate(0.1) - if time.time() > timeout: - self.skip = "couldn't start process" - else: - self.processProtocol.clearBuffer() - - def tearDownClass(self): - if hasattr(self, 'skip'): - return - test_process.SignalMixin.tearDownClass(self) - CFTPClientTestBase.tearDownClass(self) + return d def tearDown(self): d = self.stopServer() - d.addCallback(self._killProcess) + d.addCallback(lambda _: self.processProtocol.killProcess()) return d def _killProcess(self, ignored): @@ -156,141 +244,260 @@ except error.ProcessExitedAlready: pass - def _getCmdResult(self, cmd): - self.processProtocol.clearBuffer() - self.processProtocol.transport.write(cmd+'\n') - timeout = time.time() + 10 - while (self.processProtocol.buffer.find('cftp> ') == -1) and (time.time() < timeout): - reactor.iterate(0.1) - self.failIf(time.time() > timeout, "timeout") - if self.processProtocol.buffer.startswith('cftp> '): - self.processProtocol.buffer = self.processProtocol.buffer[6:] - return self.processProtocol.buffer[:-6].strip() + def runCommand(self, command): + """ + Run the given command with the cftp client. Return a C{Deferred} that + fires when the command is complete. Payload is the server's output for + that command. + """ + return self.processProtocol.runCommand(command) + + def runScript(self, *commands): + """ + Run the given commands with the cftp client. Returns a C{Deferred} + that fires when the commands are all complete. The C{Deferred}'s + payload is a list of output for each command. + """ + return self.processProtocol.runScript(commands) def testCdPwd(self): + """ + Test that 'pwd' reports the current remote directory, that 'lpwd' + reports the current local directory, and that changing to a + subdirectory then changing to its parent leaves you in the original + remote directory. + """ + # XXX - not actually a unit test, see docstring. homeDir = os.path.join(os.getcwd(), self.testDir) - pwdRes = self._getCmdResult('pwd') - lpwdRes = self._getCmdResult('lpwd') - cdRes = self._getCmdResult('cd testDirectory') - self._getCmdResult('cd ..') - pwd2Res = self._getCmdResult('pwd') - self.failUnlessEqual(pwdRes, homeDir) - self.failUnlessEqual(lpwdRes, os.getcwd()) - self.failUnlessEqual(cdRes, '') - self.failUnlessEqual(pwd2Res, pwdRes) + d = self.runScript('pwd', 'lpwd', 'cd testDirectory', 'cd ..', 'pwd') + d.addCallback(lambda xs: xs[:3] + xs[4:]) + d.addCallback(self.assertEqual, + [homeDir, os.getcwd(), '', homeDir]) + return d def testChAttrs(self): - lsRes = self._getCmdResult('ls -l testfile1') - self.failUnless(lsRes.startswith('-rw-r--r--'), lsRes) - self.failIf(self._getCmdResult('chmod 0 testfile1')) - lsRes = self._getCmdResult('ls -l testfile1') - self.failUnless(lsRes.startswith('----------'), lsRes) - self.failIf(self._getCmdResult('chmod 644 testfile1')) - log.flushErrors() - # XXX test chgrp/own + """ + Check that 'ls -l' output includes the access permissions and that + this output changes appropriately with 'chmod'. + """ + def _check(results): + self.flushLoggedErrors() + self.assertTrue(results[0].startswith('-rw-r--r--')) + self.assertEqual(results[1], '') + self.assertTrue(results[2].startswith('----------'), results[2]) + self.assertEqual(results[3], '') + + d = self.runScript('ls -l testfile1', 'chmod 0 testfile1', + 'ls -l testfile1', 'chmod 644 testfile1') + return d.addCallback(_check) + # XXX test chgrp/own + def testList(self): - lsRes = self._getCmdResult('ls').split('\n') - self.failUnlessEqual(lsRes, ['testDirectory', 'testRemoveFile', \ - 'testRenameFile', 'testfile1']) - lsRes = self._getCmdResult( - 'ls ../' + os.path.basename(self.testDir)).split('\n') - self.failUnlessEqual(lsRes, ['testDirectory', 'testRemoveFile', \ - 'testRenameFile', 'testfile1']) - lsRes = self._getCmdResult('ls *File').split('\n') - self.failUnlessEqual(lsRes, ['testRemoveFile', 'testRenameFile']) - lsRes = self._getCmdResult('ls -a *File').split('\n') - self.failUnlessEqual(lsRes, ['.testHiddenFile', 'testRemoveFile', 'testRenameFile']) - lsRes = self._getCmdResult('ls -l testDirectory') - self.failIf(lsRes) - # XXX test lls in a way that doesn't depend on local semantics + """ + Check 'ls' works as expected. Checks for wildcards, hidden files, + listing directories and listing empty directories. + """ + def _check(results): + self.assertEqual(results[0], ['testDirectory', 'testRemoveFile', + 'testRenameFile', 'testfile1']) + self.assertEqual(results[1], ['testDirectory', 'testRemoveFile', + 'testRenameFile', 'testfile1']) + self.assertEqual(results[2], ['testRemoveFile', 'testRenameFile']) + self.assertEqual(results[3], ['.testHiddenFile', 'testRemoveFile', + 'testRenameFile']) + self.assertEqual(results[4], ['']) + d = self.runScript('ls', 'ls ../' + os.path.basename(self.testDir), + 'ls *File', 'ls -a *File', 'ls -l testDirectory') + d.addCallback(lambda xs: [x.split('\n') for x in xs]) + return d.addCallback(_check) def testHelp(self): - helpRes = self._getCmdResult('?') - self.failUnlessEqual(helpRes, cftp.StdioClient(None).cmd_HELP('').strip()) + """ + Check that running the '?' command returns help. + """ + d = self.runCommand('?') + d.addCallback(self.assertEqual, + cftp.StdioClient(None).cmd_HELP('').strip()) + return d - def _failUnlessFilesEqual(self, name1, name2, msg=None): + def assertFilesEqual(self, name1, name2, msg=None): + """ + Assert that the files at C{name1} and C{name2} contain exactly the + same data. + """ f1 = file(name1).read() f2 = file(name2).read() self.failUnlessEqual(f1, f2, msg) + def testGet(self): - getRes = self._getCmdResult( - 'get testfile1 "%s/test file2"' % (self.testDir,)) - self._failUnlessFilesEqual( - self.testDir + '/testfile1', - self.testDir + '/test file2', "get failed") - self.failUnless( - getRes.endswith("Transferred %s/%s/testfile1 to %s/test file2" - % (os.getcwd(), self.testDir, self.testDir))) - self.failIf(self._getCmdResult('rm "test file2"')) - self.failIf(os.path.exists(self.testDir + '/test file2')) + """ + Test that 'get' saves the remote file to the correct local location, + that the output of 'get' is correct and that 'rm' actually removes + the file. + """ + # XXX - not actually a unit test + expectedOutput = ("Transferred %s/%s/testfile1 to %s/test file2" + % (os.getcwd(), self.testDir, self.testDir)) + def _checkGet(result): + self.assertTrue(result.endswith(expectedOutput)) + self.assertFilesEqual(self.testDir + '/testfile1', + self.testDir + '/test file2', + "get failed") + return self.runCommand('rm "test file2"') + + d = self.runCommand('get testfile1 "%s/test file2"' % (self.testDir,)) + d.addCallback(_checkGet) + d.addCallback(lambda _: self.failIf( + os.path.exists(self.testDir + '/test file2'))) + return d + def testWildcardGet(self): - getRes = self._getCmdResult('get testR*') - self._failUnlessFilesEqual( - self.testDir + '/testRemoveFile', - 'testRemoveFile', 'testRemoveFile get failed') - self._failUnlessFilesEqual( - self.testDir + '/testRenameFile', - 'testRenameFile', 'testRenameFile get failed') + """ + Test that 'get' works correctly when given wildcard parameters. + """ + def _check(ignored): + self.assertFilesEqual(self.testDir + '/testRemoveFile', + 'testRemoveFile', + 'testRemoveFile get failed') + self.assertFilesEqual(self.testDir + '/testRenameFile', + 'testRenameFile', + 'testRenameFile get failed') + + d = self.runCommand('get testR*') + return d.addCallback(_check) + def testPut(self): - putRes = self._getCmdResult( - 'put %s/testfile1 "test\\"file2"' % (self.testDir,)) - f1 = file(self.testDir + '/testfile1').read() - f2 = file(self.testDir + '/test"file2').read() - self.failUnlessEqual(f1, f2, "put failed") - self.failUnless( - putRes.endswith('Transferred %s/testfile1 to %s/%s/test"file2' - % (self.testDir, os.getcwd(), self.testDir))) - self.failIf(self._getCmdResult('rm "test\\"file2"')) - self.failIf(os.path.exists(self.testDir + '/test"file2')) + """ + Check that 'put' uploads files correctly and that they can be + successfully removed. Also check the output of the put command. + """ + # XXX - not actually a unit test + expectedOutput = ('Transferred %s/testfile1 to %s/%s/test"file2' + % (self.testDir, os.getcwd(), self.testDir)) + def _checkPut(result): + self.assertFilesEqual(self.testDir + '/testfile1', + self.testDir + '/test"file2') + self.failUnless(result.endswith(expectedOutput)) + return self.runCommand('rm "test\\"file2"') + + d = self.runCommand('put %s/testfile1 "test\\"file2"' + % (self.testDir,)) + d.addCallback(_checkPut) + d.addCallback(lambda _: self.failIf( + os.path.exists(self.testDir + '/test"file2'))) + return d + def testWildcardPut(self): - self.failIf(self._getCmdResult('cd ..')) - getRes = self._getCmdResult('put %s/testR*' % (self.testDir,)) - self._failUnlessFilesEqual( - self.testDir + '/testRemoveFile', - self.testDir + '/../testRemoveFile', 'testRemoveFile get failed') - self._failUnlessFilesEqual( - self.testDir + '/testRenameFile', - self.testDir + '/../testRenameFile', 'testRenameFile get failed') - self.failIf(self._getCmdResult('cd ' + os.path.basename(self.testDir))) + """ + What happens if you issue a 'put' command and include a wildcard (i.e. + '*') in parameter? Check that all files matching the wildcard are + uploaded to the correct directory. + """ + def check(results): + self.assertEqual(results[0], '') + self.assertEqual(results[2], '') + self.assertFilesEqual(self.testDir + '/testRemoveFile', + self.testDir + '/../testRemoveFile', + 'testRemoveFile get failed') + self.assertFilesEqual(self.testDir + '/testRenameFile', + self.testDir + '/../testRenameFile', + 'testRenameFile get failed') + + d = self.runScript('cd ..', + 'put %s/testR*' % (self.testDir,), + 'cd %s' % os.path.basename(self.testDir)) + d.addCallback(check) + return d + def testLink(self): - linkRes = self._getCmdResult('ln testLink testfile1') - self.failIf(linkRes) - lslRes = self._getCmdResult('ls -l testLink') - log.flushErrors() - self.failUnless(lslRes.startswith('l'), 'link failed') - self.failIf(self._getCmdResult('rm testLink')) - - def testDirectory(self): - self.failIf(self._getCmdResult('mkdir testMakeDirectory')) - lslRes = self._getCmdResult('ls -l testMakeDirector?') - self.failUnless(lslRes.startswith('d'), lslRes) - self.failIf(self._getCmdResult('rmdir testMakeDirectory')) - self.failIf(self._getCmdResult( - 'lmkdir %s/testLocalDirectory' % (self.testDir,))) - self.failIf(self._getCmdResult('rmdir testLocalDirectory')) + """ + Test that 'ln' creates a file which appears as a link in the output of + 'ls'. Check that removing the new file succeeds without output. + """ + def _check(results): + self.flushLoggedErrors() + self.assertEqual(results[0], '') + self.assertTrue(results[1].startswith('l'), 'link failed') + return self.runCommand('rm testLink') + + d = self.runScript('ln testLink testfile1', 'ls -l testLink') + d.addCallback(_check) + d.addCallback(self.assertEqual, '') + return d + + + def testRemoteDirectory(self): + """ + Test that we can create and remove directories with the cftp client. + """ + def _check(results): + self.assertEqual(results[0], '') + self.assertTrue(results[1].startswith('d')) + return self.runCommand('rmdir testMakeDirectory') + + d = self.runScript('mkdir testMakeDirectory', + 'ls -l testMakeDirector?') + d.addCallback(_check) + d.addCallback(self.assertEqual, '') + return d + + + def test_existingRemoteDirectory(self): + """ + Test that a C{mkdir} on an existing directory fails with the + appropriate error, and doesn't log an useless error server side. + """ + def _check(results): + self.assertEquals(results[0], '') + self.assertEquals(results[1], + 'remote error 11: mkdir failed') + + d = self.runScript('mkdir testMakeDirectory', + 'mkdir testMakeDirectory') + d.addCallback(_check) + return d + + + def testLocalDirectory(self): + """ + Test that we can create a directory locally and remove it with the + cftp client. This test works because the 'remote' server is running + out of a local directory. + """ + d = self.runCommand('lmkdir %s/testLocalDirectory' % (self.testDir,)) + d.addCallback(self.assertEqual, '') + d.addCallback(lambda _: self.runCommand('rmdir testLocalDirectory')) + d.addCallback(self.assertEqual, '') + return d + def testRename(self): - self.failIf(self._getCmdResult('rename testfile1 testfile2')) - lsRes = self._getCmdResult('ls testfile?').split('\n') - self.failUnlessEqual(lsRes, ['testfile2']) - self.failIf(self._getCmdResult('rename testfile2 testfile1')) + """ + Test that we can rename a file. + """ + def _check(results): + self.assertEqual(results[0], '') + self.assertEqual(results[1], 'testfile2') + return self.runCommand('rename testfile2 testfile1') + + d = self.runScript('rename testfile1 testfile2', 'ls testfile?') + d.addCallback(_check) + d.addCallback(self.assertEqual, '') + return d - def testCommand(self): - cmdRes = self._getCmdResult('!echo hello') - self.failUnlessEqual(cmdRes, 'hello') -class TestOurServerBatchFile(test_process.SignalMixin, CFTPClientTestBase): + def testCommand(self): + d = self.runCommand('!echo hello') + return d.addCallback(self.assertEqual, 'hello') - def setUpClass(self): - test_process.SignalMixin.setUpClass(self) - CFTPClientTestBase.setUpClass(self) +class TestOurServerBatchFile(CFTPClientTestBase): def setUp(self): CFTPClientTestBase.setUp(self) self.startServer() @@ -299,12 +506,8 @@ CFTPClientTestBase.tearDown(self) return self.stopServer() - def tearDownClass(self): - test_process.SignalMixin.tearDownClass(self) - CFTPClientTestBase.tearDownClass(self) - def _getBatchOutput(self, f): - fn = tempfile.mktemp() + fn = self.mktemp() open(fn, 'w').write(f) l = [] port = self.server.getHost().port @@ -382,15 +585,11 @@ d.addCallback(_cbCheckResult) return d -class TestOurServerUnixClient(test_process.SignalMixin, CFTPClientTestBase): - def setUpClass(self): - if hasattr(self, 'skip'): - return - test_process.SignalMixin.setUpClass(self) - CFTPClientTestBase.setUpClass(self) +class TestOurServerUnixClient(test_conch._UnixFixHome, CFTPClientTestBase): def setUp(self): + test_conch._UnixFixHome.setUp(self) CFTPClientTestBase.setUp(self) self.startServer() cmd1 = ('-p %i -l testuser ' @@ -413,17 +612,17 @@ uao = default.SSHUserAuthClient(o['user'], o, conn) return connect.connect(o['host'], int(o['port']), o, vhk, uao) - def tearDownClass(self): - test_process.SignalMixin.tearDownClass(self) - CFTPClientTestBase.tearDownClass(self) - def tearDown(self): + CFTPClientTestBase.tearDown(self) d = defer.maybeDeferred(self.conn.transport.loseConnection) d.addCallback(lambda x : self.stopServer()) - return d + def clean(ign): + test_conch._UnixFixHome.tearDown(self) + return ign + return defer.gatherResults([d, self.conn.stopDeferred]).addBoth(clean) def _getBatchOutput(self, f): - fn = tempfile.mktemp() + fn = self.mktemp() open(fn, 'w').write(f) port = self.server.getHost().port cmds = ('-p %i -l testuser ' @@ -461,7 +660,62 @@ return d + +class TestOurServerSftpClient(CFTPClientTestBase): + """ + Test the sftp server against sftp command line client. + """ + + def setUp(self): + CFTPClientTestBase.setUp(self) + return self.startServer() + + + def tearDown(self): + return self.stopServer() + + + def test_extendedAttributes(self): + """ + Test the return of extended attributes by the server: the sftp client + should ignore them, but still be able to parse the response correctly. + + This test is mainly here to check that + L{filetransfer.FILEXFER_ATTR_EXTENDED} has the correct value. + """ + fn = self.mktemp() + open(fn, 'w').write("ls .\nexit") + port = self.server.getHost().port + + oldGetAttr = FileTransferForTestAvatar._getAttrs + def _getAttrs(self, s): + attrs = oldGetAttr(self, s) + attrs["ext_foo"] = "bar" + return attrs + + self.patch(FileTransferForTestAvatar, "_getAttrs", _getAttrs) + + self.server.factory.expectedLoseConnection = True + cmds = ('-o', 'IdentityFile=dsa_test', + '-o', 'UserKnownHostsFile=kh_test', + '-o', 'HostKeyAlgorithms=ssh-rsa', + '-o', 'Port=%i' % (port,), '-b', fn, 'testuser@127.0.0.1') + d = getProcessOutputAndValue("sftp", cmds) + def check(result): + self.assertEquals(result[2], 0) + for i in ['testDirectory', 'testRemoveFile', + 'testRenameFile', 'testfile1']: + self.assertIn(i, result[0]) + return d.addCallback(check) + + + if not unix or not Crypto or not interfaces.IReactorProcess(reactor, None): TestOurServerCmdLineClient.skip = "don't run w/o spawnprocess or PyCrypto" TestOurServerBatchFile.skip = "don't run w/o spawnProcess or PyCrypto" TestOurServerUnixClient.skip = "don't run w/o spawnProcess or PyCrypto" + TestOurServerSftpClient.skip = "don't run w/o spawnProcess or PyCrypto" +else: + from twisted.python.procutils import which + if not which('sftp'): + TestOurServerSftpClient.skip = "no sftp command-line client available" diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_channel.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_channel.py --- twisted-conch-0.8.0/twisted/conch/test/test_channel.py 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_channel.py 2008-02-19 20:32:40.000000000 +0000 @@ -0,0 +1,279 @@ +# Copyright (C) 2007-2008 Twisted Matrix Laboratories +# See LICENSE for details + +""" +Test ssh/channel.py. +""" +from twisted.conch.ssh import channel +from twisted.trial import unittest + + +class MockTransport(object): + """ + A mock Transport. All we use is the getPeer() and getHost() methods. + Channels implement the ITransport interface, and their getPeer() and + getHost() methods return ('SSH', ) so + we need to implement these methods so they have something to draw + from. + """ + def getPeer(self): + return ('MockPeer',) + + def getHost(self): + return ('MockHost',) + + +class MockConnection(object): + """ + A mock for twisted.conch.ssh.connection.SSHConnection. Record the data + that channels send, and when they try to close the connection. + + @ivar data: a C{dict} mapping channel id #s to lists of data sent by that + channel. + @ivar extData: a C{dict} mapping channel id #s to lists of 2-tuples + (extended data type, data) sent by that channel. + @ivar closes: a C{dict} mapping channel id #s to True if that channel sent + a close message. + """ + transport = MockTransport() + + def __init__(self): + self.data = {} + self.extData = {} + self.closes = {} + + def logPrefix(self): + """ + Return our logging prefix. + """ + return "MockConnection" + + def sendData(self, channel, data): + """ + Record the sent data. + """ + self.data.setdefault(channel, []).append(data) + + def sendExtendedData(self, channel, type, data): + """ + Record the sent extended data. + """ + self.extData.setdefault(channel, []).append((type, data)) + + def sendClose(self, channel): + """ + Record that the channel sent a close message. + """ + self.closes[channel] = True + + +class ChannelTestCase(unittest.TestCase): + + def setUp(self): + """ + Initialize the channel. remoteMaxPacket is 10 so that data is able + to be sent (the default of 0 means no data is sent because no packets + are made). + """ + self.conn = MockConnection() + self.channel = channel.SSHChannel(conn=self.conn, + remoteMaxPacket=10) + self.channel.name = 'channel' + + def test_init(self): + """ + Test that SSHChannel initializes correctly. localWindowSize defaults + to 131072 (2**17) and localMaxPacket to 32768 (2**15) as reasonable + defaults (what OpenSSH uses for those variables). + + The values in the second set of assertions are meaningless; they serve + only to verify that the instance variables are assigned in the correct + order. + """ + c = channel.SSHChannel(conn=self.conn) + self.assertEquals(c.localWindowSize, 131072) + self.assertEquals(c.localWindowLeft, 131072) + self.assertEquals(c.localMaxPacket, 32768) + self.assertEquals(c.remoteWindowLeft, 0) + self.assertEquals(c.remoteMaxPacket, 0) + self.assertEquals(c.conn, self.conn) + self.assertEquals(c.data, None) + self.assertEquals(c.avatar, None) + + c2 = channel.SSHChannel(1, 2, 3, 4, 5, 6, 7) + self.assertEquals(c2.localWindowSize, 1) + self.assertEquals(c2.localWindowLeft, 1) + self.assertEquals(c2.localMaxPacket, 2) + self.assertEquals(c2.remoteWindowLeft, 3) + self.assertEquals(c2.remoteMaxPacket, 4) + self.assertEquals(c2.conn, 5) + self.assertEquals(c2.data, 6) + self.assertEquals(c2.avatar, 7) + + def test_str(self): + """ + Test that str(SSHChannel) works gives the channel name and local and + remote windows at a glance.. + """ + self.assertEquals(str(self.channel), '') + + def test_logPrefix(self): + """ + Test that SSHChannel.logPrefix gives the name of the channel, the + local channel ID and the underlying connection. + """ + self.assertEquals(self.channel.logPrefix(), 'SSHChannel channel ' + '(unknown) on MockConnection') + + def test_addWindowBytes(self): + """ + Test that addWindowBytes adds bytes to the window and resumes writing + if it was paused. + """ + cb = [False] + def stubStartWriting(): + cb[0] = True + self.channel.startWriting = stubStartWriting + self.channel.write('test') + self.channel.writeExtended(1, 'test') + self.channel.addWindowBytes(50) + self.assertEquals(self.channel.remoteWindowLeft, 50 - 4 - 4) + self.assertTrue(self.channel.areWriting) + self.assertTrue(cb[0]) + self.assertEquals(self.channel.buf, '') + self.assertEquals(self.conn.data[self.channel], ['test']) + self.assertEquals(self.channel.extBuf, []) + self.assertEquals(self.conn.extData[self.channel], [(1, 'test')]) + + cb[0] = False + self.channel.addWindowBytes(20) + self.assertFalse(cb[0]) + + self.channel.write('a'*80) + self.channel.loseConnection() + self.channel.addWindowBytes(20) + self.assertFalse(cb[0]) + + def test_requestReceived(self): + """ + Test that requestReceived handles requests by dispatching them to + request_* methods. + """ + self.channel.request_test_method = lambda data: data == '' + self.assertTrue(self.channel.requestReceived('test-method', '')) + self.assertFalse(self.channel.requestReceived('test-method', 'a')) + self.assertFalse(self.channel.requestReceived('bad-method', '')) + + def test_closeReceieved(self): + """ + Test that the default closeReceieved closes the connection. + """ + self.assertFalse(self.channel.closing) + self.channel.closeReceived() + self.assertTrue(self.channel.closing) + + def test_write(self): + """ + Test that write handles data correctly. Send data up to the size + of the remote window, splitting the data into packets of length + remoteMaxPacket. + """ + cb = [False] + def stubStopWriting(): + cb[0] = True + # no window to start with + self.channel.stopWriting = stubStopWriting + self.channel.write('d') + self.channel.write('a') + self.assertFalse(self.channel.areWriting) + self.assertTrue(cb[0]) + # regular write + self.channel.addWindowBytes(20) + self.channel.write('ta') + data = self.conn.data[self.channel] + self.assertEquals(data, ['da', 'ta']) + self.assertEquals(self.channel.remoteWindowLeft, 16) + # larger than max packet + self.channel.write('12345678901') + self.assertEquals(data, ['da', 'ta', '1234567890', '1']) + self.assertEquals(self.channel.remoteWindowLeft, 5) + # running out of window + cb[0] = False + self.channel.write('123456') + self.assertFalse(self.channel.areWriting) + self.assertTrue(cb[0]) + self.assertEquals(data, ['da', 'ta', '1234567890', '1', '12345']) + self.assertEquals(self.channel.buf, '6') + self.assertEquals(self.channel.remoteWindowLeft, 0) + + def test_writeExtended(self): + """ + Test that writeExtended handles data correctly. Send extended data + up to the size of the window, splitting the extended data into packets + of length remoteMaxPacket. + """ + cb = [False] + def stubStopWriting(): + cb[0] = True + # no window to start with + self.channel.stopWriting = stubStopWriting + self.channel.writeExtended(1, 'd') + self.channel.writeExtended(1, 'a') + self.channel.writeExtended(2, 't') + self.assertFalse(self.channel.areWriting) + self.assertTrue(cb[0]) + # regular write + self.channel.addWindowBytes(20) + self.channel.writeExtended(2, 'a') + data = self.conn.extData[self.channel] + self.assertEquals(data, [(1, 'da'), (2, 't'), (2, 'a')]) + self.assertEquals(self.channel.remoteWindowLeft, 16) + # larger than max packet + self.channel.writeExtended(3, '12345678901') + self.assertEquals(data, [(1, 'da'), (2, 't'), (2, 'a'), + (3, '1234567890'), (3, '1')]) + self.assertEquals(self.channel.remoteWindowLeft, 5) + # running out of window + cb[0] = False + self.channel.writeExtended(4, '123456') + self.assertFalse(self.channel.areWriting) + self.assertTrue(cb[0]) + self.assertEquals(data, [(1, 'da'), (2, 't'), (2, 'a'), + (3, '1234567890'), (3, '1'), (4, '12345')]) + self.assertEquals(self.channel.extBuf, [[4, '6']]) + self.assertEquals(self.channel.remoteWindowLeft, 0) + + def test_writeSequence(self): + """ + Test that writeSequence is equivalent to write(''.join(sequece)). + """ + self.channel.addWindowBytes(20) + self.channel.writeSequence(map(str, range(10))) + self.assertEquals(self.conn.data[self.channel], ['0123456789']) + + def test_loseConnection(self): + """ + Tesyt that loseConnection() doesn't close the channel until all + the data is sent. + """ + self.channel.write('data') + self.channel.writeExtended(1, 'datadata') + self.channel.loseConnection() + self.assertEquals(self.conn.closes.get(self.channel), None) + self.channel.addWindowBytes(4) # send regular data + self.assertEquals(self.conn.closes.get(self.channel), None) + self.channel.addWindowBytes(8) # send extended data + self.assertTrue(self.conn.closes.get(self.channel)) + + def test_getPeer(self): + """ + Test that getPeer() returns ('SSH', ). + """ + self.assertEquals(self.channel.getPeer(), ('SSH', 'MockPeer')) + + def test_getHost(self): + """ + Test that getHost() returns ('SSH', ). + """ + self.assertEquals(self.channel.getHost(), ('SSH', 'MockHost')) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_checkers.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_checkers.py --- twisted-conch-0.8.0/twisted/conch/test/test_checkers.py 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_checkers.py 2008-09-10 15:58:45.000000000 +0100 @@ -0,0 +1,107 @@ +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Tests for L{twisted.conch.checkers}. +""" + +try: + import pwd +except ImportError: + pwd = None + +import os, base64 + +from twisted.trial.unittest import TestCase +from twisted.python.filepath import FilePath +from twisted.cred.credentials import UsernamePassword +from twisted.test.test_process import MockOS + +try: + import Crypto.Cipher.DES3 +except ImportError: + SSHPublicKeyDatabase = None +else: + from twisted.conch.checkers import SSHPublicKeyDatabase + + + +class SSHPublicKeyDatabaseTests(TestCase): + """ + Tests for L{SSHPublicKeyDatabase}. + """ + + if pwd is None: + skip = "Cannot run without pwd module" + elif SSHPublicKeyDatabase is None: + skip = "Cannot run without PyCrypto" + + def setUp(self): + self.checker = SSHPublicKeyDatabase() + self.sshDir = FilePath(self.mktemp()) + self.sshDir.makedirs() + + self.key1 = base64.encodestring("foobar") + self.key2 = base64.encodestring("eggspam") + self.content = "t1 %s foo\nt2 %s egg\n" % (self.key1, self.key2) + + self.mockos = MockOS() + self.mockos.path = self.sshDir.path + self.patch(os.path, "expanduser", self.mockos.expanduser) + self.patch(pwd, "getpwnam", self.mockos.getpwnam) + self.patch(os, "seteuid", self.mockos.seteuid) + self.patch(os, "setegid", self.mockos.setegid) + + + def _testCheckKey(self, filename): + self.sshDir.child(filename).setContent(self.content) + user = UsernamePassword("user", "password") + user.blob = "foobar" + self.assertTrue(self.checker.checkKey(user)) + user.blob = "eggspam" + self.assertTrue(self.checker.checkKey(user)) + user.blob = "notallowed" + self.assertFalse(self.checker.checkKey(user)) + + + def test_checkKey(self): + """ + L{SSHPublicKeyDatabase.checkKey} should retrieve the content of the + authorized_keys file and check the keys against that file. + """ + self._testCheckKey("authorized_keys") + self.assertEquals(self.mockos.seteuidCalls, []) + self.assertEquals(self.mockos.setegidCalls, []) + + + def test_checkKey2(self): + """ + L{SSHPublicKeyDatabase.checkKey} should retrieve the content of the + authorized_keys2 file and check the keys against that file. + """ + self._testCheckKey("authorized_keys2") + self.assertEquals(self.mockos.seteuidCalls, []) + self.assertEquals(self.mockos.setegidCalls, []) + + + def test_checkKeyAsRoot(self): + """ + If the key file is readable, L{SSHPublicKeyDatabase.checkKey} should + switch its uid/gid to the ones of the authenticated user. + """ + keyFile = self.sshDir.child("authorized_keys") + keyFile.setContent(self.content) + # Fake permission error by changing the mode + keyFile.chmod(0000) + self.addCleanup(keyFile.chmod, 0777) + # And restore the right mode when seteuid is called + savedSeteuid = os.seteuid + def seteuid(euid): + keyFile.chmod(0777) + return savedSeteuid(euid) + self.patch(os, "seteuid", seteuid) + user = UsernamePassword("user", "password") + user.blob = "foobar" + self.assertTrue(self.checker.checkKey(user)) + self.assertEquals(self.mockos.seteuidCalls, [0, 1, 0, os.getuid()]) + self.assertEquals(self.mockos.setegidCalls, [2, os.getgid()]) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_conch.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_conch.py --- twisted-conch-0.8.0/twisted/conch/test/test_conch.py 2006-12-05 14:35:31.000000000 +0000 +++ twisted-conch-8.2.0/twisted/conch/test/test_conch.py 2008-09-10 15:58:45.000000000 +0100 @@ -1,136 +1,222 @@ # -*- test-case-name: twisted.conch.test.test_conch -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE for details. -import os, sys -from twisted.cred import portal -from twisted.internet import reactor, defer, protocol, error -from twisted.python import log, runtime -from twisted.trial import unittest +import os, sys, socket + try: - import Crypto + import Crypto.Cipher.DES3 except: Crypto = None -from twisted.test.test_process import SignalMixin -from test_ssh import ConchTestRealm, _LogTimeFormatMixin +from twisted.cred import portal +from twisted.internet import reactor, defer, protocol +from twisted.internet.error import ProcessExitedAlready +from twisted.python import log, runtime +from twisted.python.filepath import FilePath +from twisted.trial import unittest +from twisted.conch.error import ConchError +from twisted.conch.test.test_ssh import ConchTestRealm +from twisted.python.procutils import which + +from twisted.conch.test.keydata import publicRSA_openssh, privateRSA_openssh +from twisted.conch.test.keydata import publicDSA_openssh, privateDSA_openssh + + class Echo(protocol.Protocol): def connectionMade(self): log.msg('ECHO CONNECTION MADE') + def connectionLost(self, reason): log.msg('ECHO CONNECTION DONE') + def dataReceived(self, data): self.transport.write(data) if '\n' in data: self.transport.loseConnection() + + class EchoFactory(protocol.Factory): protocol = Echo + + class ConchTestOpenSSHProcess(protocol.ProcessProtocol): + """ + Test protocol for launching an OpenSSH client process. + + @ivar deferred: Set by whatever uses this object. Accessed using + L{_getDeferred}, which destroys the value so the Deferred is not + fired twice. Fires when the process is terminated. + """ + deferred = None buf = '' - def __init__(self, d): - self.deferred = d + def _getDeferred(self): + d, self.deferred = self.deferred, None + return d - def connectionMade(self): - log.msg('MAD(ssh): connection made') def outReceived(self, data): self.buf += data - def errReceived(self, data): - log.msg("ERR(ssh): '%s'" % data) def processEnded(self, reason): - unittest.assertEquals(reason.value.exitCode, 0, 'exit code was not 0: %s' % reason.value.exitCode) - self.buf = self.buf.replace('\r\n', '\n') - unittest.assertEquals(self.buf, 'goodbye\n') - self.deferred.callback(None) + """ + Called when the process has ended. + + @param reason: a Failure giving the reason for the process' end. + """ + if reason.value.exitCode != 0: + self._getDeferred().errback( + ConchError("exit code was not 0: %s" % + reason.value.exitCode)) + else: + buf = self.buf.replace('\r\n', '\n') + self._getDeferred().callback(buf) + class ConchTestForwardingProcess(protocol.ProcessProtocol): + """ + Manages a third-party process which launches a server. - def __init__(self, d, port, fac): - self.deferred = d + Uses L{ConchTestForwardingPort} to connect to the third-party server. + Once L{ConchTestForwardingPort} has disconnected, kill the process and fire + a Deferred with the data received by the L{ConchTestForwardingPort}. + + @ivar deferred: Set by whatever uses this object. Accessed using + L{_getDeferred}, which destroys the value so the Deferred is not + fired twice. Fires when the process is terminated. + """ + + deferred = None + + def __init__(self, port, data): + """ + @type port: C{int} + @param port: The port on which the third-party server is listening. + (it is assumed that the server is running on localhost). + + @type data: C{str} + @param data: This is sent to the third-party server. Must end with '\n' + in order to trigger a disconnect. + """ self.port = port - self.fac = fac - self.connected = 0 - self.buf = '' + self.buffer = None + self.data = data + + + def _getDeferred(self): + d, self.deferred = self.deferred, None + return d + def connectionMade(self): - reactor.callLater(1, self._connect) + self._connect() + def _connect(self): - self.connected = 1 - cc = protocol.ClientCreator(reactor, ConchTestForwardingPort, self) + """ + Connect to the server, which is often a third-party process. + Tries to reconnect if it fails because we have no way of determining + exactly when the port becomes available for listening -- we can only + know when the process starts. + """ + cc = protocol.ClientCreator(reactor, ConchTestForwardingPort, self, + self.data) d = cc.connectTCP('127.0.0.1', self.port) d.addErrback(self._ebConnect) + return d + def _ebConnect(self, f): - # probably because the server wasn't listening in time - # but who knows, just try again - log.msg('ERROR CONNECTING TO %s' % self.port) - log.err(f) - log.flushErrors() - reactor.callLater(1, self._connect) + reactor.callLater(.1, self._connect) + + + def forwardingPortDisconnected(self, buffer): + """ + The network connection has died; save the buffer of output + from the network and attempt to quit the process gracefully, + and then (after the reactor has spun) send it a KILL signal. + """ + self.buffer = buffer + self.transport.write('\x03') + self.transport.loseConnection() + reactor.callLater(0, self._reallyDie) + + + def _reallyDie(self): + try: + self.transport.signalProcess('KILL') + except ProcessExitedAlready: + pass - def errReceived(self, data): - log.msg("ERR(ssh): '%s'" % data) def processEnded(self, reason): - log.msg('FORWARDING PROCESS CLOSED') - self.deferred.callback(None) + """ + Fire the Deferred at self.deferred with the data collected + from the L{ConchTestForwardingPort} connection, if any. + """ + self._getDeferred().callback(self.buffer) -class ConchTestForwardingPort(protocol.Protocol): - data = 'test forwarding\n' +class ConchTestForwardingPort(protocol.Protocol): + """ + Connects to server launched by a third-party process (managed by + L{ConchTestForwardingProcess}) sends data, then reports whatever it + received back to the L{ConchTestForwardingProcess} once the connection + is ended. + """ + + + def __init__(self, protocol, data): + """ + @type protocol: L{ConchTestForwardingProcess} + @param protocol: The L{ProcessProtocol} which made this connection. + + @type data: str + @param data: The data to be sent to the third-party server. + """ + self.protocol = protocol + self.data = data - def __init__(self, proto): - self.proto = proto def connectionMade(self): - log.msg('FORWARDING PORT OPEN') - self.proto.fac.proto.expectedLoseConnection = 1 - self.buf = '' + self.buffer = '' self.transport.write(self.data) + def dataReceived(self, data): - self.buf += data + self.buffer += data + def connectionLost(self, reason): - log.msg('FORWARDING PORT CLOSED') - unittest.failUnlessEqual(self.buf, self.data) + self.protocol.forwardingPortDisconnected(self.buffer) - # forwarding-only clients don't die on their own - self.proto.transport.write('\x03') - self.proto.transport.loseConnection() - reactor.callLater(0, self.reallyDie) - def reallyDie(self): - try: - self.proto.transport.signalProcess('KILL') - except error.ProcessExitedAlready: - pass - -from test_keys import publicRSA_openssh, privateRSA_openssh -from test_keys import publicDSA_openssh, privateDSA_openssh if Crypto: from twisted.conch.client import options, default, connect - from twisted.conch.error import ConchError from twisted.conch.ssh import forwarding from twisted.conch.ssh import connection - from test_ssh import ConchTestServerFactory, ConchTestPublicKeyChecker + from twisted.conch.test.test_ssh import ConchTestServerFactory + from twisted.conch.test.test_ssh import ConchTestPublicKeyChecker class SSHTestConnectionForUnix(connection.SSHConnection): + """ + @ivar stopDeferred: Deferred that will be fired when C{serviceStopped} + is called. + @type stopDeferred: C{defer.Deferred} + """ def __init__(self, p, exe=None, cmds=None): connection.SSHConnection.__init__(self) @@ -140,7 +226,10 @@ self.spawn = None self.connected = 0 self.remoteForwards = {} + self.stopDeferred = defer.Deferred() + def serviceStopped(self): + self.stopDeferred.callback(None) def serviceStarted(self): if self.spawn: @@ -209,14 +298,30 @@ run()""" % mod] return start + list(args) -class CmdLineClientTestBase(SignalMixin, _LogTimeFormatMixin): + + +class ForwardingTestBase: + """ + Template class for tests of the Conch server's ability to forward arbitrary + protocols over SSH. + + These tests are integration tests, not unit tests. They launch a Conch + server, a custom TCP server (just an L{EchoProtocol}) and then call + L{execute}. + + L{execute} is implemented by subclasses of L{ForwardingTestBase}. It should + cause an SSH client to connect to the Conch server, asking it to forward + data to the custom TCP server. + """ if not Crypto: skip = "can't run w/o PyCrypto" - def setUpClass(self): - SignalMixin.setUpClass(self) - _LogTimeFormatMixin.setUpClass(self) + def _createFiles(self): + for f in ['rsa_test','rsa_test.pub','dsa_test','dsa_test.pub', + 'kh_test']: + if os.path.exists(f): + os.remove(f) open('rsa_test','w').write(privateRSA_openssh) open('rsa_test.pub','w').write(publicRSA_openssh) open('dsa_test.pub','w').write(publicDSA_openssh) @@ -225,99 +330,137 @@ os.chmod('rsa_test', 33152) open('kh_test','w').write('127.0.0.1 '+publicRSA_openssh) - def tearDownClass(self): - SignalMixin.tearDownClass(self) - _LogTimeFormatMixin.tearDownClass(self) - for f in ['rsa_test','rsa_test.pub','dsa_test','dsa_test.pub', 'kh_test']: - os.remove(f) - def setUp(self): + def _getFreePort(self): + s = socket.socket() + s.bind(('', 0)) + port = s.getsockname()[1] + s.close() + return port + + + def _makeConchFactory(self): + """ + Make a L{ConchTestServerFactory}, which allows us to start a + L{ConchTestServer} -- i.e. an actually listening conch. + """ realm = ConchTestRealm() p = portal.Portal(realm) p.registerChecker(ConchTestPublicKeyChecker()) - self.fac = fac = ConchTestServerFactory() - fac.portal = p - self.server = reactor.listenTCP(0, fac, interface="127.0.0.1") + factory = ConchTestServerFactory() + factory.portal = p + return factory + + + def setUp(self): + self._createFiles() + self.conchFactory = self._makeConchFactory() + self.conchFactory.expectedLoseConnection = 1 + self.conchServer = reactor.listenTCP(0, self.conchFactory, + interface="127.0.0.1") + self.echoServer = reactor.listenTCP(0, EchoFactory()) + self.echoPort = self.echoServer.getHost().port + def tearDown(self): try: - self.fac.proto.done = 1 + self.conchFactory.proto.done = 1 except AttributeError: pass else: - self.fac.proto.transport.loseConnection() - return defer.maybeDeferred(self.server.stopListening) + self.conchFactory.proto.transport.loseConnection() + return defer.gatherResults([ + defer.maybeDeferred(self.conchServer.stopListening), + defer.maybeDeferred(self.echoServer.stopListening)]) + + + def test_exec(self): + """ + Test that we can use whatever client to send the command "echo goodbye" + to the Conch server. Make sure we receive "goodbye" back from the + server. + """ + d = self.execute('echo goodbye', ConchTestOpenSSHProcess()) + return d.addCallback(self.assertEquals, 'goodbye\n') + + + def test_localToRemoteForwarding(self): + """ + Test that we can use whatever client to forward a local port to a + specified port on the server. + """ + localPort = self._getFreePort() + process = ConchTestForwardingProcess(localPort, 'test\n') + d = self.execute('', process, + sshArgs='-N -L%i:127.0.0.1:%i' + % (localPort, self.echoPort)) + d.addCallback(self.assertEqual, 'test\n') + return d + + + def test_remoteToLocalForwarding(self): + """ + Test that we can use whatever client to forward a port from the server + to a port locally. + """ + localPort = self._getFreePort() + process = ConchTestForwardingProcess(localPort, 'test\n') + d = self.execute('', process, + sshArgs='-N -R %i:127.0.0.1:%i' + % (localPort, self.echoPort)) + d.addCallback(self.assertEqual, 'test\n') + return d - def _getRandomPort(self): - f = EchoFactory() - serv = reactor.listenTCP(0, f) - port = serv.getHost().port - serv.stopListening() - return port - # actual tests - def testExec(self): - d = defer.Deferred() - p = ConchTestOpenSSHProcess(d) - return self.execute('echo goodbye', p) - - def testLocalToRemoteForwarding(self): - f = EchoFactory() - f.fac = self.fac - serv = reactor.listenTCP(0, f) - port = serv.getHost().port - lport = self._getRandomPort() - d = defer.Deferred() - d.addCallback(lambda x : defer.maybeDeferred(serv.stopListening)) - p = ConchTestForwardingProcess(d, lport,self.fac) - return self.execute('', p, - preargs='-N -L%i:127.0.0.1:%i' % (lport, port)) - - def testRemoteToLocalForwarding(self): - f = EchoFactory() - f.fac = self.fac - serv = reactor.listenTCP(0, f) - port = serv.getHost().port - lport = self._getRandomPort() - d = defer.Deferred() - d.addCallback(lambda x : defer.maybeDeferred(serv.stopListening)) - p = ConchTestForwardingProcess(d, lport, self.fac) - return self.execute('', p, - preargs='-N -R %i:127.0.0.1:%i' % (lport, port)) +class OpenSSHClientTestCase(ForwardingTestBase, unittest.TestCase): + def execute(self, remoteCommand, process, sshArgs=''): + """ + Connects to the SSH server started in L{ForwardingTestBase.setUp} by + running the 'ssh' command line tool. -class OpenSSHClientTestCase(CmdLineClientTestBase, unittest.TestCase): + @type remoteCommand: str + @param remoteCommand: The command (with arguments) to run on the + remote end. - def execute(self, args, p, preargs = ''): + @type process: L{ConchTestOpenSSHProcess} + + @type sshArgs: str + @param sshArgs: Arguments to pass to the 'ssh' process. + + @return: L{defer.Deferred} + """ + process.deferred = defer.Deferred() cmdline = ('ssh -2 -l testuser -p %i ' '-oUserKnownHostsFile=kh_test ' '-oPasswordAuthentication=no ' # Always use the RSA key, since that's the one in kh_test. '-oHostKeyAlgorithms=ssh-rsa ' '-a ' - '-i dsa_test ') + preargs + \ - ' 127.0.0.1 ' + args - port = self.server.getHost().port - ssh_path = None - for path in ['/usr', '', '/usr/local']: - if os.path.exists(path+'/bin/ssh'): - ssh_path = path+'/bin/ssh' - break - if not ssh_path: - log.msg('skipping test, cannot find ssh') - raise unittest.SkipTest, 'skipping test, cannot find ssh' + '-i dsa_test ') + sshArgs + \ + ' 127.0.0.1 ' + remoteCommand + port = self.conchServer.getHost().port cmds = (cmdline % port).split() - reactor.spawnProcess(p, ssh_path, cmds) - return p.deferred + reactor.spawnProcess(process, "ssh", cmds) + return process.deferred -class CmdLineClientTestCase(CmdLineClientTestBase, unittest.TestCase): - def execute(self, args, p, preargs=''): +class CmdLineClientTestCase(ForwardingTestBase, unittest.TestCase): + def setUp(self): if runtime.platformType == 'win32': - raise unittest.SkipTest, "can't run cmdline client on win32" - port = self.server.getHost().port + raise unittest.SkipTest("can't run cmdline client on win32") + ForwardingTestBase.setUp(self) + + + def execute(self, remoteCommand, process, sshArgs=''): + """ + As for L{OpenSSHClientTestCase.execute}, except it runs the 'conch' + command line tool, not 'ssh'. + """ + process.deferred = defer.Deferred() + port = self.conchServer.getHost().port cmd = ('-p %i -l testuser ' '--known-hosts kh_test ' '--user-authentications publickey ' @@ -325,47 +468,127 @@ '-a -I ' '-K direct ' '-i dsa_test ' - '-v ') % port + preargs + \ - ' 127.0.0.1 ' + args + '-v ') % port + sshArgs + \ + ' 127.0.0.1 ' + remoteCommand cmds = _makeArgs(cmd.split()) log.msg(str(cmds)) env = os.environ.copy() env['PYTHONPATH'] = os.pathsep.join(sys.path) - reactor.spawnProcess(p, sys.executable, cmds, env=env) - return p.deferred + reactor.spawnProcess(process, sys.executable, cmds, env=env) + return process.deferred + + + +class _UnixFixHome(object): + """ + Mixin class to fix the HOME environment variable to something usable. + @ivar home: FilePath pointing at C{homePath}. + @type home: L{FilePath} -class UnixClientTestCase(CmdLineClientTestBase, unittest.TestCase): + @ivar homePath: relative path to the directory used as HOME during the + tests. + @type homePath: C{str} + """ - def execute(self, args, p, preargs = ''): + def setUp(self): + path = self.mktemp() + self.home = FilePath(path) + self.homePath = os.path.join(*self.home.segmentsFrom(FilePath("."))) + if len(self.home.path) >= 70: + # UNIX_MAX_PATH is 108, and the socket file is generally of length + # 30, so we can't rely on mktemp... + self.homePath = "_tmp" + self.home = FilePath(self.homePath) + self.home.makedirs() + self.savedEnviron = os.environ.copy() + os.environ["HOME"] = self.homePath + + + def tearDown(self): + os.environ.clear() + os.environ.update(self.savedEnviron) + self.home.remove() + + + +class UnixClientTestCase(_UnixFixHome, ForwardingTestBase, unittest.TestCase): + def setUp(self): if runtime.platformType == 'win32': - raise unittest.SkipTest, "can't run cmdline client on win32" - port = self.server.getHost().port - cmd1 = ('-p %i -l testuser ' - '--known-hosts kh_test ' - '--user-authentications publickey ' - '--host-key-algorithms ssh-rsa ' - '-a ' - '-K direct ' - '-i dsa_test ' - '127.0.0.1') % port - cmd2 = ('-p %i -l testuser ' - '-K unix ' - '-v ') % port + preargs + \ - ' 127.0.0.1 ' + args - cmds1 = cmd1.split() - cmds2 = _makeArgs(cmd2.split()) + raise unittest.SkipTest("can't run cmdline client on win32") + ForwardingTestBase.setUp(self) + _UnixFixHome.setUp(self) + + + def tearDown(self): + d1 = ForwardingTestBase.tearDown(self) + d2 = defer.maybeDeferred(self.conn.transport.transport.loseConnection) + d3 = self.conn.stopDeferred + def clean(ign): + _UnixFixHome.tearDown(self) + return ign + return defer.gatherResults([d1, d2, d3]).addBoth(clean) + + + def makeOptions(self): o = options.ConchOptions() - def _(host, *args): + def parseArgs(host, *args): o['host'] = host - o.parseArgs = _ - o.parseOptions(cmds1) - vhk = default.verifyHostKey - conn = SSHTestConnectionForUnix(p, sys.executable, cmds2) - uao = default.SSHUserAuthClient(o['user'], o, conn) - d = connect.connect(o['host'], int(o['port']), o, vhk, uao) - d.addErrback(lambda f: unittest.fail('Failure connecting to test server: %s' % f)) - d.addCallback(lambda x : p.deferred) - d.addCallback(lambda x : defer.maybeDeferred( - conn.transport.transport.loseConnection)) - return d + o.parseArgs = parseArgs + return o + + + def makeAuthClient(self, port, options): + cmds = (('-p %i -l testuser ' + '--known-hosts kh_test ' + '--user-authentications publickey ' + '--host-key-algorithms ssh-rsa ' + '-a ' + '-K direct ' + '-i dsa_test ' + '127.0.0.1') % port).split() + options.parseOptions(cmds) + return default.SSHUserAuthClient(options['user'], options, self.conn) + + + def execute(self, remoteCommand, process, sshArgs=''): + """ + Connect to the forwarding process using the 'unix' client found in + L{twisted.conch.client.unix.connect}. See + L{OpenSSHClientTestCase.execute}. + """ + process.deferred = defer.Deferred() + port = self.conchServer.getHost().port + cmd = ('-p %i -l testuser ' + '-K unix ' + '-v ') % port + sshArgs + \ + ' 127.0.0.1 ' + remoteCommand + cmds = _makeArgs(cmd.split()) + options = self.makeOptions() + self.conn = SSHTestConnectionForUnix(process, sys.executable, cmds) + authClient = self.makeAuthClient(port, options) + d = connect.connect(options['host'], port, options, + default.verifyHostKey, authClient) + return d.addCallback(lambda x : process.deferred) + + + def test_noHome(self): + """ + When setting the HOME environment variable to a path that doesn't + exist, L{connect.connect} should forward the failure, and the created + process should fail with a L{ConchError}. + """ + path = self.mktemp() + # We override the HOME variable, and let tearDown restore the initial + # value + os.environ['HOME'] = path + process = ConchTestOpenSSHProcess() + d = self.execute('echo goodbye', process) + def cb(ign): + return self.assertFailure(process.deferred, ConchError) + return self.assertFailure(d, OSError).addCallback(cb) + + + +if not which('ssh'): + OpenSSHClientTestCase.skip = "no ssh command-line client available" diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_connection.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_connection.py --- twisted-conch-0.8.0/twisted/conch/test/test_connection.py 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_connection.py 2008-09-10 15:58:45.000000000 +0100 @@ -0,0 +1,623 @@ +# Copyright (c) 2007 Twisted Matrix Laboratories. +# See LICENSE for details + +""" +This module tests twisted.conch.ssh.connection. +""" + +import struct + +from twisted.conch import error +from twisted.conch.ssh import channel, common, connection +from twisted.trial import unittest +from twisted.conch.test import test_userauth + + +class TestChannel(channel.SSHChannel): + """ + A mocked-up version of twisted.conch.ssh.channel.SSHChannel. + + @ivar gotOpen: True if channelOpen has been called. + @type gotOpen: C{bool} + @ivar specificData: the specific channel open data passed to channelOpen. + @type specificData: C{str} + @ivar openFailureReason: the reason passed to openFailed. + @type openFailed: C{error.ConchError} + @ivar inBuffer: a C{list} of strings received by the channel. + @type inBuffer: C{list} + @ivar extBuffer: a C{list} of 2-tuples (type, extended data) of received by + the channel. + @type extBuffer: C{list} + @ivar numberRequests: the number of requests that have been made to this + channel. + @type numberRequests: C{int} + @ivar gotEOF: True if the other side sent EOF. + @type gotEOF: C{bool} + @ivar gotOneClose: True if the other side closed the connection. + @type gotOneClose: C{bool} + @ivar gotClosed: True if the channel is closed. + @type gotClosed: C{bool} + """ + name = "TestChannel" + gotOpen = False + + def logPrefix(self): + return "TestChannel %i" % self.id + + def channelOpen(self, specificData): + """ + The channel is open. Set up the instance variables. + """ + self.gotOpen = True + self.specificData = specificData + self.inBuffer = [] + self.extBuffer = [] + self.numberRequests = 0 + self.gotEOF = False + self.gotOneClose = False + self.gotClosed = False + + def openFailed(self, reason): + """ + Opening the channel failed. Store the reason why. + """ + self.openFailureReason = reason + + def request_test(self, data): + """ + A test request. Return True if data is 'data'. + + @type data: C{str} + """ + self.numberRequests += 1 + return data == 'data' + + def dataReceived(self, data): + """ + Data was received. Store it in the buffer. + """ + self.inBuffer.append(data) + + def extReceived(self, code, data): + """ + Extended data was received. Store it in the buffer. + """ + self.extBuffer.append((code, data)) + + def eofReceived(self): + """ + EOF was received. Remember it. + """ + self.gotEOF = True + + def closeReceived(self): + """ + Close was received. Remember it. + """ + self.gotOneClose = True + + def closed(self): + """ + The channel is closed. Rembember it. + """ + self.gotClosed = True + +class TestAvatar: + """ + A mocked-up version of twisted.conch.avatar.ConchUser + """ + + def lookupChannel(self, channelType, windowSize, maxPacket, data): + """ + The server wants us to return a channel. If the requested channel is + our TestChannel, return it, otherwise return None. + """ + if channelType == TestChannel.name: + return TestChannel(remoteWindow=windowSize, + remoteMaxPacket=maxPacket, + data=data, avatar=self) + + def gotGlobalRequest(self, requestType, data): + """ + The client has made a global request. If the global request is + 'TestGlobal', return True. If the global request is 'TestData', + return True and the request-specific data we received. Otherwise, + return False. + """ + if requestType == 'TestGlobal': + return True + elif requestType == 'TestData': + return True, data + else: + return False + +class TestConnection(connection.SSHConnection): + """ + A subclass of SSHConnection for testing. + + @ivar channel: the current channel. + @type channel. C{TestChannel} + """ + + def logPrefix(self): + return "TestConnection" + + def global_TestGlobal(self, data): + """ + The other side made the 'TestGlobal' global request. Return True. + """ + return True + + def global_Test_Data(self, data): + """ + The other side made the 'Test-Data' global request. Return True and + the data we received. + """ + return True, data + + def channel_TestChannel(self, windowSize, maxPacket, data): + """ + The other side is requesting the TestChannel. Create a C{TestChannel} + instance, store it, and return it. + """ + self.channel = TestChannel(remoteWindow=windowSize, + remoteMaxPacket=maxPacket, data=data) + return self.channel + + def channel_ErrorChannel(self, windowSize, maxPacket, data): + """ + The other side is requesting the ErrorChannel. Raise an exception. + """ + raise AssertionError('no such thing') + + + +class ConnectionTestCase(unittest.TestCase): + + if test_userauth.transport is None: + skip = "Cannot run without PyCrypto" + + def setUp(self): + self.transport = test_userauth.FakeTransport(None) + self.transport.avatar = TestAvatar() + self.conn = TestConnection() + self.conn.transport = self.transport + self.conn.serviceStarted() + + def _openChannel(self, channel): + """ + Open the channel with the default connection. + """ + self.conn.openChannel(channel) + self.transport.packets = self.transport.packets[:-1] + self.conn.ssh_CHANNEL_OPEN_CONFIRMATION(struct.pack('>2L', + channel.id, 255) + '\x00\x02\x00\x00\x00\x00\x80\x00') + + def tearDown(self): + self.conn.serviceStopped() + + def test_linkAvatar(self): + """ + Test that the connection links itself to the avatar in the + transport. + """ + self.assertIdentical(self.transport.avatar.conn, self.conn) + + def test_serviceStopped(self): + """ + Test that serviceStopped() closes any open channels. + """ + channel1 = TestChannel() + channel2 = TestChannel() + self.conn.openChannel(channel1) + self.conn.openChannel(channel2) + self.conn.ssh_CHANNEL_OPEN_CONFIRMATION('\x00\x00\x00\x00' * 4) + self.assertTrue(channel1.gotOpen) + self.assertFalse(channel2.gotOpen) + self.conn.serviceStopped() + self.assertTrue(channel1.gotClosed) + + def test_GLOBAL_REQUEST(self): + """ + Test that global request packets are dispatched to the global_* + methods and the return values are translated into success or failure + messages. + """ + self.conn.ssh_GLOBAL_REQUEST(common.NS('TestGlobal') + '\xff') + self.assertEquals(self.transport.packets, + [(connection.MSG_REQUEST_SUCCESS, '')]) + self.transport.packets = [] + self.conn.ssh_GLOBAL_REQUEST(common.NS('TestData') + '\xff' + + 'test data') + self.assertEquals(self.transport.packets, + [(connection.MSG_REQUEST_SUCCESS, 'test data')]) + self.transport.packets = [] + self.conn.ssh_GLOBAL_REQUEST(common.NS('TestBad') + '\xff') + self.assertEquals(self.transport.packets, + [(connection.MSG_REQUEST_FAILURE, '')]) + self.transport.packets = [] + self.conn.ssh_GLOBAL_REQUEST(common.NS('TestGlobal') + '\x00') + self.assertEquals(self.transport.packets, []) + + def test_REQUEST_SUCCESS(self): + """ + Test that global request success packets cause the Deferred to be + called back. + """ + d = self.conn.sendGlobalRequest('request', 'data', True) + self.conn.ssh_REQUEST_SUCCESS('data') + def check(data): + self.assertEquals(data, 'data') + d.addCallback(check) + d.addErrback(self.fail) + return d + + def test_REQUEST_FAILURE(self): + """ + Test that global request failure packets cause the Deferred to be + erred back. + """ + d = self.conn.sendGlobalRequest('request', 'data', True) + self.conn.ssh_REQUEST_FAILURE('data') + def check(f): + self.assertEquals(f.value.data, 'data') + d.addCallback(self.fail) + d.addErrback(check) + return d + + def test_CHANNEL_OPEN(self): + """ + Test that open channel packets cause a channel to be created and + opened or a failure message to be returned. + """ + del self.transport.avatar + self.conn.ssh_CHANNEL_OPEN(common.NS('TestChannel') + + '\x00\x00\x00\x01' * 4) + self.assertTrue(self.conn.channel.gotOpen) + self.assertEquals(self.conn.channel.conn, self.conn) + self.assertEquals(self.conn.channel.data, '\x00\x00\x00\x01') + self.assertEquals(self.conn.channel.specificData, '\x00\x00\x00\x01') + self.assertEquals(self.conn.channel.remoteWindowLeft, 1) + self.assertEquals(self.conn.channel.remoteMaxPacket, 1) + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_OPEN_CONFIRMATION, + '\x00\x00\x00\x01\x00\x00\x00\x00\x00\x02\x00\x00' + '\x00\x00\x80\x00')]) + self.transport.packets = [] + self.conn.ssh_CHANNEL_OPEN(common.NS('BadChannel') + + '\x00\x00\x00\x02' * 4) + self.flushLoggedErrors() + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_OPEN_FAILURE, + '\x00\x00\x00\x02\x00\x00\x00\x03' + common.NS( + 'unknown channel') + common.NS(''))]) + self.transport.packets = [] + self.conn.ssh_CHANNEL_OPEN(common.NS('ErrorChannel') + + '\x00\x00\x00\x02' * 4) + self.flushLoggedErrors() + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_OPEN_FAILURE, + '\x00\x00\x00\x02\x00\x00\x00\x02' + common.NS( + 'unknown failure') + common.NS(''))]) + + def test_CHANNEL_OPEN_CONFIRMATION(self): + """ + Test that channel open confirmation packets cause the channel to be + notified that it's open. + """ + channel = TestChannel() + self.conn.openChannel(channel) + self.conn.ssh_CHANNEL_OPEN_CONFIRMATION('\x00\x00\x00\x00'*5) + self.assertEquals(channel.remoteWindowLeft, 0) + self.assertEquals(channel.remoteMaxPacket, 0) + self.assertEquals(channel.specificData, '\x00\x00\x00\x00') + self.assertEquals(self.conn.channelsToRemoteChannel[channel], + 0) + self.assertEquals(self.conn.localToRemoteChannel[0], 0) + + def test_CHANNEL_OPEN_FAILURE(self): + """ + Test that channel open failure packets cause the channel to be + notified that its opening failed. + """ + channel = TestChannel() + self.conn.openChannel(channel) + self.conn.ssh_CHANNEL_OPEN_FAILURE('\x00\x00\x00\x00\x00\x00\x00' + '\x01' + common.NS('failure!')) + self.assertEquals(channel.openFailureReason.args, ('failure!', 1)) + self.assertEquals(self.conn.channels.get(channel), None) + + + def test_CHANNEL_WINDOW_ADJUST(self): + """ + Test that channel window adjust messages add bytes to the channel + window. + """ + channel = TestChannel() + self._openChannel(channel) + oldWindowSize = channel.remoteWindowLeft + self.conn.ssh_CHANNEL_WINDOW_ADJUST('\x00\x00\x00\x00\x00\x00\x00' + '\x01') + self.assertEquals(channel.remoteWindowLeft, oldWindowSize + 1) + + def test_CHANNEL_DATA(self): + """ + Test that channel data messages are passed up to the channel, or + cause the channel to be closed if the data is too large. + """ + channel = TestChannel(localWindow=6, localMaxPacket=5) + self._openChannel(channel) + self.conn.ssh_CHANNEL_DATA('\x00\x00\x00\x00' + common.NS('data')) + self.assertEquals(channel.inBuffer, ['data']) + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_WINDOW_ADJUST, '\x00\x00\x00\xff' + '\x00\x00\x00\x04')]) + self.transport.packets = [] + longData = 'a' * (channel.localWindowLeft + 1) + self.conn.ssh_CHANNEL_DATA('\x00\x00\x00\x00' + common.NS(longData)) + self.assertEquals(channel.inBuffer, ['data']) + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_CLOSE, '\x00\x00\x00\xff')]) + channel = TestChannel() + self._openChannel(channel) + bigData = 'a' * (channel.localMaxPacket + 1) + self.transport.packets = [] + self.conn.ssh_CHANNEL_DATA('\x00\x00\x00\x01' + common.NS(bigData)) + self.assertEquals(channel.inBuffer, []) + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_CLOSE, '\x00\x00\x00\xff')]) + + def test_CHANNEL_EXTENDED_DATA(self): + """ + Test that channel extended data messages are passed up to the channel, + or cause the channel to be closed if they're too big. + """ + channel = TestChannel(localWindow=6, localMaxPacket=5) + self._openChannel(channel) + self.conn.ssh_CHANNEL_EXTENDED_DATA('\x00\x00\x00\x00\x00\x00\x00' + '\x00' + common.NS('data')) + self.assertEquals(channel.extBuffer, [(0, 'data')]) + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_WINDOW_ADJUST, '\x00\x00\x00\xff' + '\x00\x00\x00\x04')]) + self.transport.packets = [] + longData = 'a' * (channel.localWindowLeft + 1) + self.conn.ssh_CHANNEL_EXTENDED_DATA('\x00\x00\x00\x00\x00\x00\x00' + '\x00' + common.NS(longData)) + self.assertEquals(channel.extBuffer, [(0, 'data')]) + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_CLOSE, '\x00\x00\x00\xff')]) + channel = TestChannel() + self._openChannel(channel) + bigData = 'a' * (channel.localMaxPacket + 1) + self.transport.packets = [] + self.conn.ssh_CHANNEL_EXTENDED_DATA('\x00\x00\x00\x01\x00\x00\x00' + '\x00' + common.NS(bigData)) + self.assertEquals(channel.extBuffer, []) + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_CLOSE, '\x00\x00\x00\xff')]) + + def test_CHANNEL_EOF(self): + """ + Test that channel eof messages are passed up to the channel. + """ + channel = TestChannel() + self._openChannel(channel) + self.conn.ssh_CHANNEL_EOF('\x00\x00\x00\x00') + self.assertTrue(channel.gotEOF) + + def test_CHANNEL_CLOSE(self): + """ + Test that channel close messages are passed up to the channel. Also, + test that channel.close() is called if both sides are closed when this + message is received. + """ + channel = TestChannel() + self._openChannel(channel) + self.conn.sendClose(channel) + self.conn.ssh_CHANNEL_CLOSE('\x00\x00\x00\x00') + self.assertTrue(channel.gotOneClose) + self.assertTrue(channel.gotClosed) + + def test_CHANNEL_REQUEST_success(self): + """ + Test that channel requests that succeed send MSG_CHANNEL_SUCCESS. + """ + channel = TestChannel() + self._openChannel(channel) + self.conn.ssh_CHANNEL_REQUEST('\x00\x00\x00\x00' + common.NS('test') + + '\x00') + self.assertEquals(channel.numberRequests, 1) + d = self.conn.ssh_CHANNEL_REQUEST('\x00\x00\x00\x00' + common.NS( + 'test') + '\xff' + 'data') + def check(result): + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_SUCCESS, '\x00\x00\x00\xff')]) + d.addCallback(check) + return d + + def test_CHANNEL_REQUEST_failure(self): + """ + Test that channel requests that fail send MSG_CHANNEL_FAILURE. + """ + channel = TestChannel() + self._openChannel(channel) + d = self.conn.ssh_CHANNEL_REQUEST('\x00\x00\x00\x00' + common.NS( + 'test') + '\xff') + def check(result): + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_FAILURE, '\x00\x00\x00\xff' + )]) + d.addCallback(self.fail) + d.addErrback(check) + return d + + def test_CHANNEL_REQUEST_SUCCESS(self): + """ + Test that channel request success messages cause the Deferred to be + called back. + """ + channel = TestChannel() + self._openChannel(channel) + d = self.conn.sendRequest(channel, 'test', 'data', True) + self.conn.ssh_CHANNEL_SUCCESS('\x00\x00\x00\x00') + def check(result): + self.assertTrue(result) + return d + + def test_CHANNEL_REQUEST_FAILURE(self): + """ + Test that channel request failure messages cause the Deferred to be + erred back. + """ + channel = TestChannel() + self._openChannel(channel) + d = self.conn.sendRequest(channel, 'test', '', True) + self.conn.ssh_CHANNEL_FAILURE('\x00\x00\x00\x00') + def check(result): + self.assertEquals(result.value.value, 'channel request failed') + d.addCallback(self.fail) + d.addErrback(check) + return d + + def test_sendGlobalRequest(self): + """ + Test that global request messages are sent in the right format. + """ + d = self.conn.sendGlobalRequest('wantReply', 'data', True) + self.conn.sendGlobalRequest('noReply', '', False) + self.assertEquals(self.transport.packets, + [(connection.MSG_GLOBAL_REQUEST, common.NS('wantReply') + + '\xffdata'), + (connection.MSG_GLOBAL_REQUEST, common.NS('noReply') + + '\x00')]) + self.assertEquals(self.conn.deferreds, {'global':[d]}) + + def test_openChannel(self): + """ + Test that open channel messages are sent in the right format. + """ + channel = TestChannel() + self.conn.openChannel(channel, 'aaaa') + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_OPEN, common.NS('TestChannel') + + '\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x80\x00aaaa')]) + self.assertEquals(channel.id, 0) + self.assertEquals(self.conn.localChannelID, 1) + + def test_sendRequest(self): + """ + Test that channel request messages are sent in the right format. + """ + channel = TestChannel() + self._openChannel(channel) + d = self.conn.sendRequest(channel, 'test', 'test', True) + self.conn.sendRequest(channel, 'test2', '', False) + channel.localClosed = True # emulate sending a close message + self.conn.sendRequest(channel, 'test3', '', True) + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_REQUEST, '\x00\x00\x00\xff' + + common.NS('test') + '\x01test'), + (connection.MSG_CHANNEL_REQUEST, '\x00\x00\x00\xff' + + common.NS('test2') + '\x00')]) + self.assertEquals(self.conn.deferreds, {0:[d]}) + + def test_adjustWindow(self): + """ + Test that channel window adjust messages cause bytes to be added + to the window. + """ + channel = TestChannel(localWindow=5) + self._openChannel(channel) + channel.localWindowLeft = 0 + self.conn.adjustWindow(channel, 1) + self.assertEquals(channel.localWindowLeft, 1) + channel.localClosed = True + self.conn.adjustWindow(channel, 2) + self.assertEquals(channel.localWindowLeft, 1) + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_WINDOW_ADJUST, '\x00\x00\x00\xff' + '\x00\x00\x00\x01')]) + + def test_sendData(self): + """ + Test that channel data messages are sent in the right format. + """ + channel = TestChannel() + self._openChannel(channel) + self.conn.sendData(channel, 'a') + channel.localClosed = True + self.conn.sendData(channel, 'b') + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_DATA, '\x00\x00\x00\xff' + + common.NS('a'))]) + + def test_sendExtendedData(self): + """ + Test that channel extended data messages are sent in the right format. + """ + channel = TestChannel() + self._openChannel(channel) + self.conn.sendExtendedData(channel, 1, 'test') + channel.localClosed = True + self.conn.sendExtendedData(channel, 2, 'test2') + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_EXTENDED_DATA, '\x00\x00\x00\xff' + + '\x00\x00\x00\x01' + common.NS('test'))]) + + def test_sendEOF(self): + """ + Test that channel EOF messages are sent in the right format. + """ + channel = TestChannel() + self._openChannel(channel) + self.conn.sendEOF(channel) + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_EOF, '\x00\x00\x00\xff')]) + channel.localClosed = True + self.conn.sendEOF(channel) + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_EOF, '\x00\x00\x00\xff')]) + + def test_sendClose(self): + """ + Test that channel close messages are sent in the right format. + """ + channel = TestChannel() + self._openChannel(channel) + self.conn.sendClose(channel) + self.assertTrue(channel.localClosed) + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_CLOSE, '\x00\x00\x00\xff')]) + self.conn.sendClose(channel) + self.assertEquals(self.transport.packets, + [(connection.MSG_CHANNEL_CLOSE, '\x00\x00\x00\xff')]) + + channel2 = TestChannel() + self._openChannel(channel2) + channel2.remoteClosed = True + self.conn.sendClose(channel2) + self.assertTrue(channel2.gotClosed) + + def test_getChannelWithAvatar(self): + """ + Test that getChannel dispatches to the avatar when an avatar is + present. Correct functioning without the avatar is verified in + test_CHANNEL_OPEN. + """ + channel = self.conn.getChannel('TestChannel', 50, 30, 'data') + self.assertEquals(channel.data, 'data') + self.assertEquals(channel.remoteWindowLeft, 50) + self.assertEquals(channel.remoteMaxPacket, 30) + self.assertRaises(error.ConchError, self.conn.getChannel, + 'BadChannel', 50, 30, 'data') + + def test_gotGlobalRequestWithoutAvatar(self): + """ + Test that gotGlobalRequests dispatches to global_* without an avatar. + """ + del self.transport.avatar + self.assertTrue(self.conn.gotGlobalRequest('TestGlobal', 'data')) + self.assertEquals(self.conn.gotGlobalRequest('Test-Data', 'data'), + (True, 'data')) + self.assertFalse(self.conn.gotGlobalRequest('BadGlobal', 'data')) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_filetransfer.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_filetransfer.py --- twisted-conch-0.8.0/twisted/conch/test/test_filetransfer.py 2006-10-08 23:23:48.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_filetransfer.py 2008-11-01 15:32:22.000000000 +0000 @@ -1,13 +1,17 @@ # -*- test-case-name: twisted.conch.test.test_filetransfer -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE file for details. + import os +import re +import struct import sys from twisted.trial import unittest try: from twisted.conch import unix + unix # shut up pyflakes except ImportError: unix = None try: @@ -18,19 +22,17 @@ pass from twisted.conch import avatar -from twisted.conch.ssh import filetransfer, session -from twisted.internet import defer, reactor +from twisted.conch.ssh import common, connection, filetransfer, session +from twisted.internet import defer from twisted.protocols import loopback -from twisted.python import components, log - +from twisted.python import components -class FileTransferTestAvatar(avatar.ConchUser): - def __init__(self, homeDir): +class TestAvatar(avatar.ConchUser): + def __init__(self): avatar.ConchUser.__init__(self) self.channelLookup['session'] = session.SSHSession self.subsystemLookup['sftp'] = filetransfer.FileTransferServer - self.homeDir = homeDir def _runAsUser(self, f, *args, **kw): try: @@ -44,9 +46,17 @@ r = func(*args, **kw) return r + +class FileTransferTestAvatar(TestAvatar): + + def __init__(self, homeDir): + TestAvatar.__init__(self) + self.homeDir = homeDir + def getHomeDir(self): return os.path.join(os.getcwd(), self.homeDir) + class ConchSessionForTestAvatar: def __init__(self, avatar): @@ -73,7 +83,7 @@ raise NotImplementedError components.registerAdapter(FileTransferForTestAvatar, - FileTransferTestAvatar, + TestAvatar, filetransfer.ISFTPServer) class SFTPTestBase(unittest.TestCase): @@ -122,18 +132,73 @@ self._emptyBuffers() + def _emptyBuffers(self): while self.serverTransport.buffer or self.clientTransport.buffer: self.serverTransport.clearBuffer() self.clientTransport.clearBuffer() - def _delayedEmptyBuffers(self): - reactor.callLater(0.1, self._emptyBuffers) + + def tearDown(self): + self.serverTransport.loseConnection() + self.clientTransport.loseConnection() + self.serverTransport.clearBuffer() + self.clientTransport.clearBuffer() + def testServerVersion(self): self.failUnlessEqual(self._serverVersion, 3) self.failUnlessEqual(self._extData, {'conchTest' : 'ext data'}) + + def test_openedFileClosedWithConnection(self): + """ + A file opened with C{openFile} is close when the connection is lost. + """ + d = self.client.openFile("testfile1", filetransfer.FXF_READ | + filetransfer.FXF_WRITE, {}) + self._emptyBuffers() + + oldClose = os.close + closed = [] + def close(fd): + closed.append(fd) + oldClose(fd) + + self.patch(os, "close", close) + + def _fileOpened(openFile): + fd = self.server.openFiles[openFile.handle[4:]].fd + self.serverTransport.loseConnection() + self.clientTransport.loseConnection() + self.serverTransport.clearBuffer() + self.clientTransport.clearBuffer() + self.assertEquals(self.server.openFiles, {}) + self.assertIn(fd, closed) + + d.addCallback(_fileOpened) + return d + + + def test_openedDirectoryClosedWithConnection(self): + """ + A directory opened with C{openDirectory} is close when the connection + is lost. + """ + d = self.client.openDirectory('') + self._emptyBuffers() + + def _getFiles(openDir): + self.serverTransport.loseConnection() + self.clientTransport.loseConnection() + self.serverTransport.clearBuffer() + self.clientTransport.clearBuffer() + self.assertEquals(self.server.openDirs, {}) + + d.addCallback(_getFiles) + return d + + def testOpenFileIO(self): d = self.client.openFile("testfile1", filetransfer.FXF_READ | filetransfer.FXF_WRITE, {}) @@ -177,7 +242,7 @@ return d def _err(f): - log.flushErrors() + self.flushLoggedErrors() return f def _close(openFile): @@ -239,6 +304,30 @@ d.addCallback(_getAttrs) return d + + def test_openFileExtendedAttributes(self): + """ + Check that L{filetransfer.FileTransferClient.openFile} can send + extended attributes, that should be extracted server side. By default, + they are ignored, so we just verify they are correctly parsed. + """ + savedAttributes = {} + oldOpenFile = self.server.client.openFile + def openFile(filename, flags, attrs): + savedAttributes.update(attrs) + return oldOpenFile(filename, flags, attrs) + self.server.client.openFile = openFile + + d = self.client.openFile("testfile1", filetransfer.FXF_READ | + filetransfer.FXF_WRITE, {"ext_foo": "bar"}) + self._emptyBuffers() + + def check(ign): + self.assertEquals(savedAttributes, {"ext_foo": "bar"}) + + return d.addCallback(check) + + def testRemoveFile(self): d = self.client.getAttrs("testRemoveFile") self._emptyBuffers() @@ -375,3 +464,214 @@ d = self.client.extendedRequest('testBadRequest', '') self._emptyBuffers() return self.assertFailure(d, NotImplementedError) + + +class FakeConn: + def sendClose(self, channel): + pass + + +class TestFileTransferClose(unittest.TestCase): + + if not unix: + skip = "can't run on non-posix computers" + + def setUp(self): + self.avatar = TestAvatar() + + def buildServerConnection(self): + # make a server connection + conn = connection.SSHConnection() + # server connections have a 'self.transport.avatar'. + class DummyTransport: + def __init__(self): + self.transport = self + def sendPacket(self, kind, data): + pass + def logPrefix(self): + return 'dummy transport' + conn.transport = DummyTransport() + conn.transport.avatar = self.avatar + return conn + + def interceptConnectionLost(self, sftpServer): + self.connectionLostFired = False + origConnectionLost = sftpServer.connectionLost + def connectionLost(reason): + self.connectionLostFired = True + origConnectionLost(reason) + sftpServer.connectionLost = connectionLost + + def assertSFTPConnectionLost(self): + self.assertTrue(self.connectionLostFired, + "sftpServer's connectionLost was not called") + + def test_sessionClose(self): + """ + Closing a session should notify an SFTP subsystem launched by that + session. + """ + # make a session + testSession = session.SSHSession(conn=FakeConn(), avatar=self.avatar) + + # start an SFTP subsystem on the session + testSession.request_subsystem(common.NS('sftp')) + sftpServer = testSession.client.transport.proto + + # intercept connectionLost so we can check that it's called + self.interceptConnectionLost(sftpServer) + + # close session + testSession.closeReceived() + + self.assertSFTPConnectionLost() + + def test_clientClosesChannelOnConnnection(self): + """ + A client sending CHANNEL_CLOSE should trigger closeReceived on the + associated channel instance. + """ + conn = self.buildServerConnection() + + # somehow get a session + packet = common.NS('session') + struct.pack('>L', 0) * 3 + conn.ssh_CHANNEL_OPEN(packet) + sessionChannel = conn.channels[0] + + sessionChannel.request_subsystem(common.NS('sftp')) + sftpServer = sessionChannel.client.transport.proto + self.interceptConnectionLost(sftpServer) + + # intercept closeReceived + self.interceptConnectionLost(sftpServer) + + # close the connection + conn.ssh_CHANNEL_CLOSE(struct.pack('>L', 0)) + + self.assertSFTPConnectionLost() + + + def test_stopConnectionServiceClosesChannel(self): + """ + Closing an SSH connection should close all sessions within it. + """ + conn = self.buildServerConnection() + + # somehow get a session + packet = common.NS('session') + struct.pack('>L', 0) * 3 + conn.ssh_CHANNEL_OPEN(packet) + sessionChannel = conn.channels[0] + + sessionChannel.request_subsystem(common.NS('sftp')) + sftpServer = sessionChannel.client.transport.proto + self.interceptConnectionLost(sftpServer) + + # close the connection + conn.serviceStopped() + + self.assertSFTPConnectionLost() + + + +class TestConstants(unittest.TestCase): + """ + Tests for the constants used by the SFTP protocol implementation. + + @ivar filexferSpecExcerpts: Excerpts from the + draft-ietf-secsh-filexfer-02.txt (draft) specification of the SFTP + protocol. There are more recent drafts of the specification, but this + one describes version 3, which is what conch (and OpenSSH) implements. + """ + + + filexferSpecExcerpts = [ + """ + The following values are defined for packet types. + + #define SSH_FXP_INIT 1 + #define SSH_FXP_VERSION 2 + #define SSH_FXP_OPEN 3 + #define SSH_FXP_CLOSE 4 + #define SSH_FXP_READ 5 + #define SSH_FXP_WRITE 6 + #define SSH_FXP_LSTAT 7 + #define SSH_FXP_FSTAT 8 + #define SSH_FXP_SETSTAT 9 + #define SSH_FXP_FSETSTAT 10 + #define SSH_FXP_OPENDIR 11 + #define SSH_FXP_READDIR 12 + #define SSH_FXP_REMOVE 13 + #define SSH_FXP_MKDIR 14 + #define SSH_FXP_RMDIR 15 + #define SSH_FXP_REALPATH 16 + #define SSH_FXP_STAT 17 + #define SSH_FXP_RENAME 18 + #define SSH_FXP_READLINK 19 + #define SSH_FXP_SYMLINK 20 + #define SSH_FXP_STATUS 101 + #define SSH_FXP_HANDLE 102 + #define SSH_FXP_DATA 103 + #define SSH_FXP_NAME 104 + #define SSH_FXP_ATTRS 105 + #define SSH_FXP_EXTENDED 200 + #define SSH_FXP_EXTENDED_REPLY 201 + + Additional packet types should only be defined if the protocol + version number (see Section ``Protocol Initialization'') is + incremented, and their use MUST be negotiated using the version + number. However, the SSH_FXP_EXTENDED and SSH_FXP_EXTENDED_REPLY + packets can be used to implement vendor-specific extensions. See + Section ``Vendor-Specific-Extensions'' for more details. + """, + """ + The flags bits are defined to have the following values: + + #define SSH_FILEXFER_ATTR_SIZE 0x00000001 + #define SSH_FILEXFER_ATTR_UIDGID 0x00000002 + #define SSH_FILEXFER_ATTR_PERMISSIONS 0x00000004 + #define SSH_FILEXFER_ATTR_ACMODTIME 0x00000008 + #define SSH_FILEXFER_ATTR_EXTENDED 0x80000000 + + """, + """ + The `pflags' field is a bitmask. The following bits have been + defined. + + #define SSH_FXF_READ 0x00000001 + #define SSH_FXF_WRITE 0x00000002 + #define SSH_FXF_APPEND 0x00000004 + #define SSH_FXF_CREAT 0x00000008 + #define SSH_FXF_TRUNC 0x00000010 + #define SSH_FXF_EXCL 0x00000020 + """, + """ + Currently, the following values are defined (other values may be + defined by future versions of this protocol): + + #define SSH_FX_OK 0 + #define SSH_FX_EOF 1 + #define SSH_FX_NO_SUCH_FILE 2 + #define SSH_FX_PERMISSION_DENIED 3 + #define SSH_FX_FAILURE 4 + #define SSH_FX_BAD_MESSAGE 5 + #define SSH_FX_NO_CONNECTION 6 + #define SSH_FX_CONNECTION_LOST 7 + #define SSH_FX_OP_UNSUPPORTED 8 + """] + + + def test_constantsAgainstSpec(self): + """ + The constants used by the SFTP protocol implementation match those + found by searching through the spec. + """ + constants = {} + for excerpt in self.filexferSpecExcerpts: + for line in excerpt.splitlines(): + m = re.match('^\s*#define SSH_([A-Z_]+)\s+([0-9x]*)\s*$', line) + if m: + constants[m.group(1)] = long(m.group(2), 0) + self.assertTrue( + len(constants) > 0, "No constants found (the test must be buggy).") + for k, v in constants.items(): + self.assertEqual(v, getattr(filetransfer, k)) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_helper.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_helper.py --- twisted-conch-0.8.0/twisted/conch/test/test_helper.py 2006-06-16 04:34:57.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_helper.py 2008-04-22 13:51:57.000000000 +0100 @@ -4,7 +4,7 @@ from twisted.conch.insults import helper from twisted.conch.insults.insults import G0, G1, G2, G3 -from twisted.conch.insults.insults import modes +from twisted.conch.insults.insults import modes, privateModes from twisted.conch.insults.insults import NORMAL, BOLD, UNDERLINE, BLINK, REVERSE_VIDEO from twisted.trial import unittest @@ -24,6 +24,74 @@ '\n' * (HEIGHT - 1)) self.assertEquals(self.term.reportCursorPosition(), (0, 0)) + + def test_initialPrivateModes(self): + """ + Verify that only DEC Auto Wrap Mode (DECAWM) and DEC Text Cursor Enable + Mode (DECTCEM) are initially in the Set Mode (SM) state. + """ + self.assertEqual( + {privateModes.AUTO_WRAP: True, + privateModes.CURSOR_MODE: True}, + self.term.privateModes) + + + def test_carriageReturn(self): + """ + C{"\r"} moves the cursor to the first column in the current row. + """ + self.term.cursorForward(5) + self.term.cursorDown(3) + self.assertEqual(self.term.reportCursorPosition(), (5, 3)) + self.term.insertAtCursor("\r") + self.assertEqual(self.term.reportCursorPosition(), (0, 3)) + + + def test_linefeed(self): + """ + C{"\n"} moves the cursor to the next row without changing the column. + """ + self.term.cursorForward(5) + self.assertEqual(self.term.reportCursorPosition(), (5, 0)) + self.term.insertAtCursor("\n") + self.assertEqual(self.term.reportCursorPosition(), (5, 1)) + + + def test_newline(self): + """ + C{write} transforms C{"\n"} into C{"\r\n"}. + """ + self.term.cursorForward(5) + self.term.cursorDown(3) + self.assertEqual(self.term.reportCursorPosition(), (5, 3)) + self.term.write("\n") + self.assertEqual(self.term.reportCursorPosition(), (0, 4)) + + + def test_setPrivateModes(self): + """ + Verify that L{helper.TerminalBuffer.setPrivateModes} changes the Set + Mode (SM) state to "set" for the private modes it is passed. + """ + expected = self.term.privateModes.copy() + self.term.setPrivateModes([privateModes.SCROLL, privateModes.SCREEN]) + expected[privateModes.SCROLL] = True + expected[privateModes.SCREEN] = True + self.assertEqual(expected, self.term.privateModes) + + + def test_resetPrivateModes(self): + """ + Verify that L{helper.TerminalBuffer.resetPrivateModes} changes the Set + Mode (SM) state to "reset" for the private modes it is passed. + """ + expected = self.term.privateModes.copy() + self.term.resetPrivateModes([privateModes.AUTO_WRAP, privateModes.CURSOR_MODE]) + del expected[privateModes.AUTO_WRAP] + del expected[privateModes.CURSOR_MODE] + self.assertEqual(expected, self.term.privateModes) + + def testCursorDown(self): self.term.cursorDown(3) self.assertEquals(self.term.reportCursorPosition(), (0, 3)) @@ -133,7 +201,11 @@ self.term.reverseIndex() self.assertEquals(self.term.reportCursorPosition(), (0, 1)) - def testNextLine(self): + def test_nextLine(self): + """ + C{nextLine} positions the cursor at the beginning of the row below the + current row. + """ self.term.nextLine() self.assertEquals(self.term.reportCursorPosition(), (0, 1)) self.term.cursorForward(5) @@ -486,4 +558,3 @@ self.assertEquals(len(result), 1) self.assertEquals(result[0].group(), "zoom") - diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_insults.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_insults.py --- twisted-conch-0.8.0/twisted/conch/test/test_insults.py 2006-10-08 23:23:48.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_insults.py 2008-08-16 13:03:30.000000000 +0100 @@ -58,14 +58,15 @@ return attrValue class MockMixin: - def assertCall(self, occurrence, methodName, args=(), kw={}): + def assertCall(self, occurrence, methodName, expectedPositionalArgs=(), + expectedKeywordArgs={}): attr, mock = occurrence self.assertEquals(attr, methodName) self.assertEquals(len(occurrences(mock)), 1) [(call, result, args, kw)] = occurrences(mock) self.assertEquals(call, "__call__") - self.assertEquals(args, args) - self.assertEquals(kw, kw) + self.assertEquals(args, expectedPositionalArgs) + self.assertEquals(kw, expectedKeywordArgs) return result @@ -360,3 +361,100 @@ # This isn't really an interesting assert, since it only tests that # our mock setup is working right, but I'll include it anyway. self.assertEquals(result, (6, 7)) + + + def test_applicationDataBytes(self): + """ + Contiguous non-control bytes are passed to a single call to the + C{write} method of the terminal to which the L{ClientProtocol} is + connected. + """ + occs = occurrences(self.proto) + self.parser.dataReceived('a') + self.assertCall(occs.pop(0), "write", ("a",)) + self.parser.dataReceived('bc') + self.assertCall(occs.pop(0), "write", ("bc",)) + + + def _applicationDataTest(self, data, calls): + occs = occurrences(self.proto) + self.parser.dataReceived(data) + while calls: + self.assertCall(occs.pop(0), *calls.pop(0)) + self.assertFalse(occs, "No other calls should happen: %r" % (occs,)) + + + def test_shiftInAfterApplicationData(self): + """ + Application data bytes followed by a shift-in command are passed to a + call to C{write} before the terminal's C{shiftIn} method is called. + """ + self._applicationDataTest( + 'ab\x15', [ + ("write", ("ab",)), + ("shiftIn",)]) + + + def test_shiftOutAfterApplicationData(self): + """ + Application data bytes followed by a shift-out command are passed to a + call to C{write} before the terminal's C{shiftOut} method is called. + """ + self._applicationDataTest( + 'ab\x14', [ + ("write", ("ab",)), + ("shiftOut",)]) + + + def test_cursorBackwardAfterApplicationData(self): + """ + Application data bytes followed by a cursor-backward command are passed + to a call to C{write} before the terminal's C{cursorBackward} method is + called. + """ + self._applicationDataTest( + 'ab\x08', [ + ("write", ("ab",)), + ("cursorBackward",)]) + + + def test_escapeAfterApplicationData(self): + """ + Application data bytes followed by an escape character are passed to a + call to C{write} before the terminal's handler method for the escape is + called. + """ + # Test a short escape + self._applicationDataTest( + 'ab\x1bD', [ + ("write", ("ab",)), + ("index",)]) + + # And a long escape + self._applicationDataTest( + 'ab\x1b[4h', [ + ("write", ("ab",)), + ("setModes", ([4],))]) + + # There's some other cases too, but they're all handled by the same + # codepaths as above. + + + +class ServerProtocolOutputTests(unittest.TestCase): + """ + Tests for the bytes L{ServerProtocol} writes to its transport when its + methods are called. + """ + def test_nextLine(self): + """ + L{ServerProtocol.nextLine} writes C{"\r\n"} to its transport. + """ + # Why doesn't it write ESC E? Because ESC E is poorly supported. For + # example, gnome-terminal (many different versions) fails to scroll if + # it receives ESC E and the cursor is already on the last row. + protocol = ServerProtocol() + transport = StringTransport() + protocol.makeConnection(transport) + protocol.nextLine() + self.assertEqual(transport.value(), "\r\n") diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_keys.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_keys.py --- twisted-conch-0.8.0/twisted/conch/test/test_keys.py 2005-09-19 11:15:07.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_keys.py 2008-11-16 02:54:52.000000000 +0000 @@ -1,48 +1,21 @@ -# -*- test-case-name: twisted.conch.test.test_keys -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE for details. +""" +Tests for L{twisted.conch.ssh.keys}. +""" + try: - import Crypto + import Crypto.Cipher.DES3 except ImportError: Crypto = None +else: + from twisted.conch.ssh import keys, common, sexpy, asn1 -from twisted.conch.ssh import keys +from twisted.conch.test import keydata +from twisted.python import randbytes from twisted.trial import unittest - -publicRSA_openssh = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEArzJx8OYOnJmzf4tfBEvLi8DVPrJ3/c9k2I/Az64fxjHf9imyRJbixtQhlH9lfNjUIx+4LmrJH5QNRsFporcHDKOTwTTYLh5KmRpslkYHRivcJSkbh/C+BR3utDS555mV comment" - -privateRSA_openssh = """-----BEGIN RSA PRIVATE KEY----- -MIIByAIBAAJhAK8ycfDmDpyZs3+LXwRLy4vA1T6yd/3PZNiPwM+uH8Yx3/YpskSW -4sbUIZR/ZXzY1CMfuC5qyR+UDUbBaaK3Bwyjk8E02C4eSpkabJZGB0Yr3CUpG4fw -vgUd7rQ0ueeZlQIBIwJgbh+1VZfr7WftK5lu7MHtqE1S1vPWZQYE3+VUn8yJADyb -Z4fsZaCrzW9lkIqXkE3GIY+ojdhZhkO1gbG0118sIgphwSWKRxK0mvh6ERxKqIt1 -xJEJO74EykXZV4oNJ8sjAjEA3J9r2ZghVhGN6V8DnQrTk24Td0E8hU8AcP0FVP+8 -PQm/g/aXf2QQkQT+omdHVEJrAjEAy0pL0EBH6EVS98evDCBtQw22OZT52qXlAwZ2 -gyTriKFVoqjeEjt3SZKKqXHSApP/AjBLpF99zcJJZRq2abgYlf9lv1chkrWqDHUu -DZttmYJeEfiFBBavVYIF1dOlZT0G8jMCMBc7sOSZodFnAiryP+Qg9otSBjJ3bQML -pSTqy7c3a2AScC/YyOwkDaICHnnD3XyjMwIxALRzl0tQEKMXs6hH8ToUdlLROCrP -EhQ0wahUTCk1gKA4uPD6TMTChavbh4K63OvbKg== ------END RSA PRIVATE KEY-----""" - -publicDSA_openssh = "ssh-dss AAAAB3NzaC1kc3MAAABBAIbwTOSsZ7Bl7U1KyMNqV13Tu7yRAtTr70PVI3QnfrPumf2UzCgpL1ljbKxSfAi05XvrE/1vfCFAsFYXRZLhQy0AAAAVAM965Akmo6eAi7K+k9qDR4TotFAXAAAAQADZlpTW964haQWS4vC063NGdldT6xpUGDcDRqbm90CoPEa2RmNOuOqi8lnbhYraEzypYH3K4Gzv/bxCBnKtHRUAAABAK+1osyWBS0+P90u/rAuko6chZ98thUSY2kLSHp6hLKyy2bjnT29h7haELE+XHfq2bM9fckDx2FLOSIJzy83VmQ== comment" - -privateDSA_openssh = """-----BEGIN DSA PRIVATE KEY----- -MIH4AgEAAkEAhvBM5KxnsGXtTUrIw2pXXdO7vJEC1OvvQ9UjdCd+s+6Z/ZTMKCkv -WWNsrFJ8CLTle+sT/W98IUCwVhdFkuFDLQIVAM965Akmo6eAi7K+k9qDR4TotFAX -AkAA2ZaU1veuIWkFkuLwtOtzRnZXU+saVBg3A0am5vdAqDxGtkZjTrjqovJZ24WK -2hM8qWB9yuBs7/28QgZyrR0VAkAr7WizJYFLT4/3S7+sC6SjpyFn3y2FRJjaQtIe -nqEsrLLZuOdPb2HuFoQsT5cd+rZsz19yQPHYUs5IgnPLzdWZAhUAl1TqdmlAG/b4 -nnVchGiO9sML8MM= ------END DSA PRIVATE KEY-----""" - -publicRSA_lsh = """{KDEwOnB1YmxpYy1rZXkoMTQ6cnNhLXBrY3MxLXNoYTEoMTpuNjU6AJidzg8akh9enh1JrIQyL8mrqfnJT3sBxhDkIFXqjlyN2OK2al2s5mRVNMrhzL7rX8hptPX597nHmfAS65yA85cpKDE6ZTQ6PTiAYykpKQ==}""" - -privateRSA_lsh = """(11:private-key(9:rsa-pkcs1(1:n65:\x00\x98\x9d\xce\x0f\x1a\x92\x1f^\x9e\x1dI\xac\x842/\xc9\xab\xa9\xf9\xc9O{\x01\xc6\x10\xe4 U\xea\x8e\\\x8d\xd8\xe2\xb6j]\xac\xe6dU4\xca\xe1\xcc\xbe\xeb_\xc8i\xb4\xf5\xf9\xf7\xb9\xc7\x99\xf0\x12\xeb\x9c\x80\xf3\x97)(1:e4:=8\x80c)(1:d64:h>)i\xb7\xc3z_\x94\xd30\xbd\xdf\xf5\x9d\x8d\xd7\xb4\xb2*\xcb\xef\xae~yq\xb8\x8a\xda\xae\xdf\xa3h\x9a=6{c\xb9\xf4\xa5\xe9\xe0\xf9a\xf5\xe7$*\x83\r\x1e\xcb[\xc8\xda\n\xa1\x94+\x00\x96d\xfb)(1:p33:\x00\xfd\x92\xdf\xdb\xd6\xebU\x82\xc6\x86eq9Dv\x98B\xd6\xfd\xa7\xa8,\x99\x1e\xa3\x88>\xa4A\xb7;i)(1:q33:\x00\x9a\x13\xa3\t\xd1@u\x86\xe9\xdeZym\xa8\x9c\xba\xcb\x18\x8c\xfcwJ*\x08\x0c\xac\xee\x0bU[\xd6\xff)(1:a33:\x00\xc4\xe3w\xe4\xbc\xf1q\x16\x84%D*]\xd0\x8d\xa2\xaf\x99\xff\x11\xf5\x8f\x06\xd5\x8c\xa6FH\xfe\x8e\xea\x8b)(1:b32:qx\xbd\xa6\x88\x13p\x94W\xfd\xbff\x941\xc3\xac\xa8\xaf\xe6\xaavO+\x95\xa7\x06|\x91~\xc5\xc7\xb1)(1:c32:9z\xf1\x80\xbdLE\x8c?\x8f\xd3\xe8\x05\x12\xc2@\xedZ\xec/\xb9\x8c\xdd\x07\xccM\x88g\x05jG2)))""" - -publicDSA_lsh = """{KDEwOnB1YmxpYy1rZXkoMzpkc2EoMTpwNjU6AOiMNL79iqUfSqaIHIySHKt4Jlc272yYTzAXmEg77NCgtyfDjuAcHHgwTphBA1l53i/4AAiaUBcU8qPY/Ug/MPcpKDE6cTIxOgDYKP8uLv/m6aUDAA7l5hjMq6Iy7ykoMTpnNjU6ANLKfX/CG7L9o7TQzwLa/X/hb1ZZ+++bySGQep5Ka2lCLm+gff3erqKdxwn5kjqEWq/tXtnSx3rl3TgiwO5R1GEpKDE6eTY1OgDZKD/rhxonz8sugmAcf/wIIhq4M4A+XFOzkEHj0XWHGpjycC8moBWwsIXRuRYCjbl5dA6wVv+xDrf9c6a6GMhhKSkp}""" - -privateDSA_lsh = """(11:private-key(3:dsa(1:p65:\x00\xe8\x8c4\xbe\xfd\x8a\xa5\x1fJ\xa6\x88\x1c\x8c\x92\x1c\xabx&W6\xefl\x98O0\x17\x98H;\xec\xd0\xa0\xb7\'\xc3\x8e\xe0\x1c\x1cx0N\x98A\x03Yy\xde/\xf8\x00\x08\x9aP\x17\x14\xf2\xa3\xd8\xfdH?0\xf7)(1:q21:\x00\xd8(\xff..\xff\xe6\xe9\xa5\x03\x00\x0e\xe5\xe6\x18\xcc\xab\xa22\xef)(1:g65:\x00\xd2\xca}\x7f\xc2\x1b\xb2\xfd\xa3\xb4\xd0\xcf\x02\xda\xfd\x7f\xe1oVY\xfb\xef\x9b\xc9!\x90z\x9eJkiB.o\xa0}\xfd\xde\xae\xa2\x9d\xc7\t\xf9\x92:\x84Z\xaf\xed^\xd9\xd2\xc7z\xe5\xdd8"\xc0\xeeQ\xd4a)(1:y65:\x00\xd9(?\xeb\x87\x1a\'\xcf\xcb.\x82`\x1c\x7f\xfc\x08"\x1a\xb83\x80>\\S\xb3\x90A\xe3\xd1u\x87\x1a\x98\xf2p/&\xa0\x15\xb0\xb0\x85\xd1\xb9\x16\x02\x8d\xb9yt\x0e\xb0V\xff\xb1\x0e\xb7\xfds\xa6\xba\x18\xc8a)(1:x20:>\xbb\xe4D\xb9\xb8\xb5\xf8\xf2-}\xf7\x0f\x90`\x968\xd3\x98Q)))""" +import sha, os, base64 class SSHKeysHandlingTestCase(unittest.TestCase): """ @@ -50,64 +23,883 @@ assumed test keys are in test/ """ - if not Crypto: + if Crypto is None: skip = "cannot run w/o PyCrypto" - def testDSA(self): - """test DSA keys + def setUp(self): + self.tmpdir = self.mktemp() + os.mkdir(self.tmpdir) + self.privateKeyFile = os.path.join(self.tmpdir, 'private') + self.publicKeyFile = os.path.join(self.tmpdir, 'public') + file(self.privateKeyFile, 'wb').write(keydata.privateRSA_openssh) + file(self.publicKeyFile, 'wb').write('first line\n' + + keydata.publicRSA_openssh) + + def test_readFile(self): + """ + Test that reading a key from a file works as expected. + """ + self.assertEquals(self.assertWarns(DeprecationWarning, + "getPublicKeyString is deprecated since Twisted Conch 0.9. " + "Use Key.fromString().", unittest.__file__, + keys.getPublicKeyString, self.publicKeyFile, 1), + keys.Key.fromString(keydata.publicRSA_openssh).blob()) + self.assertEquals(self.assertWarns(DeprecationWarning, + "getPrivateKeyObject is deprecated since Twisted Conch 0.9. " + "Use Key.fromString().", unittest.__file__, + keys.getPrivateKeyObject, self.privateKeyFile), + keys.Key.fromString(keydata.privateRSA_openssh).keyObject) + + def test_DSA(self): + """ + Test DSA keys using both OpenSSH and LSH formats. + """ + self._testKey(keydata.publicDSA_openssh, keydata.privateDSA_openssh, + keydata.DSAData, 'openssh') + self._testKey(keydata.publicDSA_lsh, keydata.privateDSA_lsh, + keydata.DSAData,'lsh') + obj = self.assertWarns(DeprecationWarning, "getPrivateKeyObject is " + "deprecated since Twisted Conch 0.9. Use Key.fromString().", + unittest.__file__, keys.getPrivateKeyObject, + data=keydata.privateDSA_agentv3) + self._testGeneratePrivateKey(obj, keydata.privateDSA_agentv3, + 'agentv3') + + def test_RSA(self): + """ + Same as test_DSA but for RSA keys. + """ + self._testKey(keydata.publicRSA_openssh, keydata.privateRSA_openssh, + keydata.RSAData, 'openssh') + self._testKey(keydata.publicRSA_lsh, keydata.privateRSA_lsh, + keydata.RSAData, 'lsh') + obj = self.assertWarns(DeprecationWarning, "getPrivateKeyObject is " + "deprecated since Twisted Conch 0.9. Use Key.fromString().", + unittest.__file__, keys.getPrivateKeyObject, + data=keydata.privateRSA_agentv3) + self._testGeneratePrivateKey(obj, keydata.privateRSA_agentv3, + 'agentv3') + + + def test_fingerprint(self): + """ + L{Key.fingerprint} returns a hex-encoded colon-separated md5 sum of the + public key. + """ + self.assertEquals( + '3d:13:5f:cb:c9:79:8a:93:06:27:65:bc:3d:0b:8f:af', + keys.Key.fromString(keydata.publicRSA_openssh).fingerprint()) + + + def _testKey(self, pubStr, privStr, data, keyType): + """ + Run each of the key tests with the public/private keypairs. + + @param pubStr: The data for a public key in the format defined by + keyType. + @param privStr: The data for a private key in the format defined by + keyType. + @param data: The numerical values encoded in the key. + @param keyType: the type of the public and private key data: either + "openssh" or "lsh". """ - self._testKey(publicDSA_openssh, privateDSA_openssh, 'openssh') - self._testKey(publicDSA_lsh, privateDSA_lsh, 'lsh') + pubBlob = self.assertWarns(DeprecationWarning, "getPublicKeyString is " + "deprecated since Twisted Conch 0.9. Use Key.fromString().", + unittest.__file__, keys.getPublicKeyString, data=pubStr) + pubObj = self.assertWarns(DeprecationWarning, "getPublicKeyObject is " + "deprecated since Twisted Conch 0.9. Use Key.fromString().", + unittest.__file__, keys.getPublicKeyObject, pubBlob) + privObj = self.assertWarns(DeprecationWarning, "getPrivateKeyObject is " + "deprecated since Twisted Conch 0.9. Use Key.fromString().", + unittest.__file__, keys.getPrivateKeyObject, data=privStr) + + self._testKeySignVerify(privObj, pubObj) + self._testKeyFromString(privObj, pubObj, data, keyType) + self._testGeneratePublicKey(privObj, pubObj, pubStr, keyType) + self._testGeneratePrivateKey(privObj, privStr, keyType) + self._testGenerateBlob(privObj, pubObj, pubBlob) - def testRSA(self): - """test RSA keys + def _testKeySignVerify(self, privObj, pubObj): + """ + Test that signing and verifying works correctly. + @param privObj: a private key object. + @type privObj: C{Crypto.PublicKey.pubkey.pubkey} + @param pubObj: a public key object. + @type pubObj: C{Crypto.PublicKey.pubkey.pubkey} """ - self._testKey(publicRSA_openssh, privateRSA_openssh, 'openssh') - self._testKey(publicRSA_lsh, privateRSA_lsh, 'lsh') - - def _testKey(self, pubData, privData, keyType): - privKey = keys.getPrivateKeyObject(data = privData) - pubStr = keys.getPublicKeyString(data = pubData) - pubKey = keys.getPublicKeyObject(pubStr) - self._testKeySignVerify(privKey, pubKey) - self._testKeyFromString(privKey, pubKey, privData, pubData) - self._testGenerateKey(privKey, pubKey, privData, pubData, keyType) - def _testKeySignVerify(self, priv, pub): testData = 'this is the test data' - sig = keys.signData(priv, testData) - self.assert_(keys.verifySignature(priv, sig, testData), + sig = self.assertWarns(DeprecationWarning, + "signData is deprecated since Twisted Conch 0.9. " + "Use Key(obj).sign(data).", unittest.__file__, keys.signData, + privObj, testData) + self.assertTrue(self.assertWarns(DeprecationWarning, + "verifySignature is deprecated since Twisted Conch 0.9. " + "Use Key(obj).verify(signature, data).", unittest.__file__, + keys.verifySignature, privObj, sig, testData), 'verifying with private %s failed' % - keys.objectType(priv)) - self.assert_(keys.verifySignature(pub, sig, testData), + keys.objectType(privObj)) + + self.assertTrue(self.assertWarns(DeprecationWarning, + "verifySignature is deprecated since Twisted Conch 0.9. " + "Use Key(obj).verify(signature, data).", unittest.__file__, + keys.verifySignature, pubObj, sig, testData), 'verifying with public %s failed' % - keys.objectType(pub)) - self.failIf(keys.verifySignature(priv, sig, 'other data'), + keys.objectType(pubObj)) + + self.failIf(self.assertWarns(DeprecationWarning, + "verifySignature is deprecated since Twisted Conch 0.9. " + "Use Key(obj).verify(signature, data).", unittest.__file__, + keys.verifySignature,privObj, sig, 'other data'), 'verified bad data with %s' % - keys.objectType(priv)) - self.failIf(keys.verifySignature(priv, 'bad sig', testData), + keys.objectType(privObj)) + + self.failIf(self.assertWarns(DeprecationWarning, + "verifySignature is deprecated since Twisted Conch 0.9. " + "Use Key(obj).verify(signature, data).", unittest.__file__, + keys.verifySignature, privObj, 'bad sig', testData), 'verified badsign with %s' % - keys.objectType(priv)) + keys.objectType(privObj)) + + def _testKeyFromString(self, privObj, pubObj, data, keyType): + """ + Test key object generation from a string. The public key objects + were generated in _testKey; just check that they were created + correctly. + """ + for k in data.keys(): + self.assertEquals(getattr(privObj, k), data[k]) + for k in pubObj.keydata: + if hasattr(pubObj, k): # public key objects don't have all the + # attributes + self.assertEquals(getattr(pubObj, k), data[k]) + + def _testGeneratePublicKey(self, privObj, pubObj, pubStr, keyType): + """ + Test public key string generation from an object. + """ + self.assertEquals(self.assertWarns(DeprecationWarning, + "makePublicKeyString is deprecated since Twisted Conch 0.9. " + "Use Key(obj).toString().", unittest.__file__, + keys.makePublicKeyString, pubObj, 'comment', + keyType), pubStr) + self.assertEquals(self.assertWarns(DeprecationWarning, + "makePublicKeyString is deprecated since Twisted Conch 0.9. " + "Use Key(obj).toString().", unittest.__file__, + keys.makePublicKeyString, privObj, 'comment', + keyType), pubStr) + + def _testGeneratePrivateKey(self, privObj, privStr, keyType): + """ + Test private key string generation from an object. + """ + self.assertEquals(self.assertWarns(DeprecationWarning, + "makePrivateKeyString is deprecated since Twisted Conch 0.9. " + "Use Key(obj).toString().", unittest.__file__, + keys.makePrivateKeyString, privObj, kind=keyType), + privStr) + if keyType == 'openssh': + encData = self.assertWarns(DeprecationWarning, + "makePrivateKeyString is deprecated since Twisted Conch " + "0.9. Use Key(obj).toString().", unittest.__file__, + keys.makePrivateKeyString, privObj, passphrase='test', + kind=keyType) + self.assertEquals(self.assertWarns(DeprecationWarning, + "getPrivateKeyObject is deprecated since Twisted Conch 0.9. " + "Use Key.fromString().", unittest.__file__, + keys.getPrivateKeyObject, data = encData, passphrase='test'), + privObj) + + def _testGenerateBlob(self, privObj, pubObj, pubBlob): + """ + Test wire-format blob generation. + """ + self.assertEquals(self.assertWarns(DeprecationWarning, + "makePublicKeyBlob is deprecated since Twisted Conch 0.9. " + "Use Key(obj).blob().", unittest.__file__, + keys.makePublicKeyBlob, pubObj), pubBlob) + self.assertEquals(self.assertWarns(DeprecationWarning, + "makePublicKeyBlob is deprecated since Twisted Conch 0.9. " + "Use Key(obj).blob().", unittest.__file__, + keys.makePublicKeyBlob, privObj), pubBlob) + + def test_getPublicKeyStringErrors(self): + """ + Test that getPublicKeyString raises errors in appropriate cases. + """ + self.assertWarns(DeprecationWarning, "getPublicKeyString is deprecated" + " since Twisted Conch 0.9. Use Key.fromString().", + unittest.__file__, self.assertRaises, keys.BadKeyError, + keys.getPublicKeyString, self.publicKeyFile, 1, + data=keydata.publicRSA_openssh) + self.assertWarns(DeprecationWarning, "getPublicKeyString is deprecated" + " since Twisted Conch 0.9. Use Key.fromString().", + unittest.__file__, self.assertRaises, keys.BadKeyError, + keys.getPublicKeyString, data = 'invalid key') + sexp = sexpy.pack([['public-key', ['bad-key', ['p', '2']]]]) + self.assertWarns(DeprecationWarning, "getPublicKeyString is deprecated" + " since Twisted Conch 0.9. Use Key.fromString().", + unittest.__file__, self.assertRaises, keys.BadKeyError, + keys.getPublicKeyString, data='{'+base64.encodestring(sexp)+'}') + + def test_getPrivateKeyObjectErrors(self): + """ + Test that getPrivateKeyObject raises errors in appropriate cases. + """ + self.assertWarns(DeprecationWarning, "getPrivateKeyObject is deprecated" + " since Twisted Conch 0.9. Use Key.fromString().", + unittest.__file__, self.assertRaises, keys.BadKeyError, + keys.getPrivateKeyObject, self.privateKeyFile, + keydata.privateRSA_openssh) + self.assertWarns(DeprecationWarning, "getPrivateKeyObject is deprecated" + " since Twisted Conch 0.9. Use Key.fromString().", + unittest.__file__, self.assertRaises, keys.BadKeyError, + keys.getPrivateKeyObject, data = 'invalid key') + sexp = sexpy.pack([['private-key', ['bad-key', ['p', '2']]]]) + self.assertWarns(DeprecationWarning, "getPrivateKeyObject is deprecated" + " since Twisted Conch 0.9. Use Key.fromString().", + unittest.__file__, self.assertRaises, keys.BadKeyError, + keys.getPrivateKeyObject, data=sexp) + self.assertWarns(DeprecationWarning, "getPrivateKeyObject is deprecated" + " since Twisted Conch 0.9. Use Key.fromString().", + unittest.__file__, self.assertRaises, keys.BadKeyError, + keys.getPrivateKeyObject, + data='\x00\x00\x00\x07ssh-foo'+'\x00\x00\x00\x01\x01'*5) + + def test_makePublicKeyStringErrors(self): + """ + Test that makePublicKeyString raises errors in appropriate cases. + """ + self.assertWarns(DeprecationWarning, "makePublicKeyString is deprecated" + " since Twisted Conch 0.9. Use Key(obj).toString().", + unittest.__file__, self.assertRaises, Exception, + keys.makePublicKeyString, None, kind='bad type') + self.assertWarns(DeprecationWarning, "makePublicKeyString is deprecated" + " since Twisted Conch 0.9. Use Key(obj).toString().", + unittest.__file__, self.assertRaises, Exception, + keys.makePublicKeyString, None) + self.assertWarns(DeprecationWarning, "makePublicKeyString is deprecated" + " since Twisted Conch 0.9. Use Key(obj).toString().", + unittest.__file__, self.assertRaises, Exception, + keys.makePublicKeyString, None, kind='lsh') + + def test_getPublicKeyObjectErrors(self): + """ + Test that getPublicKeyObject raises errors in appropriate cases. + """ + self.assertWarns(DeprecationWarning, "getPublicKeyObject is deprecated" + " since Twisted Conch 0.9. Use Key.fromString().", + unittest.__file__, self.assertRaises, keys.BadKeyError, + keys.getPublicKeyObject, '\x00\x00\x00\x01A') + + def test_makePrivateKeyStringErrors(self): + """ + Test that makePrivateKeyString raises errors in appropriate cases. + """ + self.assertWarns(DeprecationWarning, "makePrivateKeyString is " + "deprecated since Twisted Conch 0.9. Use Key(obj).toString().", + unittest.__file__, self.assertRaises, Exception, + keys.makePrivateKeyString, None, kind='bad type') + self.assertWarns(DeprecationWarning, "makePrivateKeyString is " + "deprecated since Twisted Conch 0.9. Use Key(obj).toString().", + unittest.__file__, self.assertRaises, Exception, + keys.makePrivateKeyString, None) + self.assertWarns(DeprecationWarning, "makePrivateKeyString is " + "deprecated since Twisted Conch 0.9. Use Key(obj).toString().", + unittest.__file__, self.assertRaises, Exception, + keys.makePrivateKeyString, None, kind='lsh') + +class HelpersTestCase(unittest.TestCase): + + if Crypto is None: + skip = "cannot run w/o PyCrypto" + + def setUp(self): + self._secureRandom = randbytes.secureRandom + randbytes.secureRandom = lambda x: '\x55' * x + + def tearDown(self): + randbytes.secureRandom = self._secureRandom + self._secureRandom = None + + def test_pkcs1(self): + """ + Test Public Key Cryptographic Standard #1 functions. + """ + data = 'ABC' + messageSize = 6 + self.assertEquals(keys.pkcs1Pad(data, messageSize), + '\x01\xff\x00ABC') + hash = sha.new().digest() + messageSize = 40 + self.assertEquals(keys.pkcs1Digest('', messageSize), + '\x01\xff\xff\xff\x00' + keys.ID_SHA1 + hash) + + def _signRSA(self, data): + key = keys.Key.fromString(keydata.privateRSA_openssh) + sig = key.sign(data) + return key.keyObject, sig + + def _signDSA(self, data): + key = keys.Key.fromString(keydata.privateDSA_openssh) + sig = key.sign(data) + return key.keyObject, sig + + def test_signRSA(self): + """ + Test that RSA keys return appropriate signatures. + """ + data = 'data' + key, sig = self._signRSA(data) + sigData = keys.pkcs1Digest(data, keys.lenSig(key)) + v = key.sign(sigData, '')[0] + self.assertEquals(sig, common.NS('ssh-rsa') + common.MP(v)) + return key, sig + + def test_signDSA(self): + """ + Test that DSA keys return appropriate signatures. + """ + data = 'data' + key, sig = self._signDSA(data) + sigData = sha.new(data).digest() + v = key.sign(sigData, '\x55' * 19) + self.assertEquals(sig, common.NS('ssh-dss') + common.NS( + Crypto.Util.number.long_to_bytes(v[0], 20) + + Crypto.Util.number.long_to_bytes(v[1], 20))) + return key, sig + + def test_verifyRSA(self): + """ + Test that RSA signatures are verified appropriately. + """ + data = 'data' + key, sig = self._signRSA(data) + self.assertTrue(self.assertWarns(DeprecationWarning, "verifySignature " + "is deprecated since Twisted Conch 0.9. Use " + "Key(obj).verify(signature, data).", unittest.__file__, + keys.verifySignature, key, sig, data)) + + def test_verifyDSA(self): + """ + Test that RSA signatures are verified appropriately. + """ + data = 'data' + key, sig = self._signDSA(data) + self.assertTrue(self.assertWarns(DeprecationWarning, "verifySignature " + "is deprecated since Twisted Conch 0.9. Use " + "Key(obj).verify(signature, data).", unittest.__file__, + keys.verifySignature, key, sig, data)) + + def test_objectType(self): + """ + Test that objectType, returns the correct type for objects. + """ + self.assertEquals(keys.objectType(keys.Key.fromString( + keydata.privateRSA_openssh).keyObject), 'ssh-rsa') + self.assertEquals(keys.objectType(keys.Key.fromString( + keydata.privateDSA_openssh).keyObject), 'ssh-dss') + self.assertRaises(keys.BadKeyError, keys.objectType, None) + + def test_asn1PackError(self): + """ + L{asn1.pack} should raise a C{ValueError} when given a type not + handled. + """ + self.assertRaises(ValueError, asn1.pack, [object()]) + + def test_printKey(self): + """ + Test that the printKey function prints correctly. + """ + obj = keys.Key.fromString(keydata.privateRSA_openssh).keyObject + self.assertEquals(self.assertWarns(DeprecationWarning, "printKey is " + "deprecated since Twisted Conch 0.9. Use repr(Key(obj)).", + unittest.__file__, keys.printKey, obj), + """RSA Private Key (767 bits) +attr e: +\t23 +attr d: +\t6e:1f:b5:55:97:eb:ed:67:ed:2b:99:6e:ec:c1:ed: +\ta8:4d:52:d6:f3:d6:65:06:04:df:e5:54:9f:cc:89: +\t00:3c:9b:67:87:ec:65:a0:ab:cd:6f:65:90:8a:97: +\t90:4d:c6:21:8f:a8:8d:d8:59:86:43:b5:81:b1:b4: +\td7:5f:2c:22:0a:61:c1:25:8a:47:12:b4:9a:f8:7a: +\t11:1c:4a:a8:8b:75:c4:91:09:3b:be:04:ca:45:d9: +\t57:8a:0d:27:cb:23 +attr n: +\t00:af:32:71:f0:e6:0e:9c:99:b3:7f:8b:5f:04:4b: +\tcb:8b:c0:d5:3e:b2:77:fd:cf:64:d8:8f:c0:cf:ae: +\t1f:c6:31:df:f6:29:b2:44:96:e2:c6:d4:21:94:7f: +\t65:7c:d8:d4:23:1f:b8:2e:6a:c9:1f:94:0d:46:c1: +\t69:a2:b7:07:0c:a3:93:c1:34:d8:2e:1e:4a:99:1a: +\t6c:96:46:07:46:2b:dc:25:29:1b:87:f0:be:05:1d: +\tee:b4:34:b9:e7:99:95 +attr q: +\t00:dc:9f:6b:d9:98:21:56:11:8d:e9:5f:03:9d:0a: +\td3:93:6e:13:77:41:3c:85:4f:00:70:fd:05:54:ff: +\tbc:3d:09:bf:83:f6:97:7f:64:10:91:04:fe:a2:67: +\t47:54:42:6b +attr p: +\t00:cb:4a:4b:d0:40:47:e8:45:52:f7:c7:af:0c:20: +\t6d:43:0d:b6:39:94:f9:da:a5:e5:03:06:76:83:24: +\teb:88:a1:55:a2:a8:de:12:3b:77:49:92:8a:a9:71: +\td2:02:93:ff +attr u: +\t00:b4:73:97:4b:50:10:a3:17:b3:a8:47:f1:3a:14: +\t76:52:d1:38:2a:cf:12:14:34:c1:a8:54:4c:29:35: +\t80:a0:38:b8:f0:fa:4c:c4:c2:85:ab:db:87:82:ba: +\tdc:eb:db:2a""") + +class KeyTestCase(unittest.TestCase): + + if Crypto is None: + skip = "cannot run w/o PyCrypto" + + def setUp(self): + self.rsaObj = Crypto.PublicKey.RSA.construct((1L, 2L, 3L, 4L, 5L)) + self.dsaObj = Crypto.PublicKey.DSA.construct((1L, 2L, 3L, 4L, 5L)) + self.rsaSignature = ('\x00\x00\x00\x07ssh-rsa\x00' + '\x00\x00`N\xac\xb4@qK\xa0(\xc3\xf2h \xd3\xdd\xee6Np\x9d_' + '\xb0>\xe3\x0c(L\x9d{\txUd|!\xf6m\x9c\xd3\x93\x842\x7fU' + '\x05\xf4\xf7\xfaD\xda\xce\x81\x8ea\x7f=Y\xed*\xb7\xba\x81' + '\xf2\xad\xda\xeb(\x97\x03S\x08\x81\xc7\xb1\xb7\xe6\xe3' + '\xcd*\xd4\xbd\xc0wt\xf7y\xcd\xf0\xb7\x7f\xfb\x1e>\xf9r' + '\x8c\xba') + self.dsaSignature = ('\x00\x00\x00\x07ssh-dss\x00\x00' + '\x00(\x18z)H\x8a\x1b\xc6\r\xbbq\xa2\xd7f\x7f$\xa7\xbf' + '\xe8\x87\x8c\x88\xef\xd9k\x1a\x98\xdd{=\xdec\x18\t\xe3' + '\x87\xa9\xc72h\x95') + self.oldSecureRandom = randbytes.secureRandom + randbytes.secureRandom = lambda x: '\xff' * x + self.keyFile = self.mktemp() + file(self.keyFile, 'wb').write(keydata.privateRSA_lsh) + + def tearDown(self): + randbytes.secureRandom = self.oldSecureRandom + del self.oldSecureRandom + os.unlink(self.keyFile) + + def test__guessStringType(self): + """ + Test that the _guessStringType method guesses string types + correctly. + """ + self.assertEquals(keys.Key._guessStringType(keydata.publicRSA_openssh), + 'public_openssh') + self.assertEquals(keys.Key._guessStringType(keydata.publicDSA_openssh), + 'public_openssh') + self.assertEquals(keys.Key._guessStringType( + keydata.privateRSA_openssh), 'private_openssh') + self.assertEquals(keys.Key._guessStringType( + keydata.privateDSA_openssh), 'private_openssh') + self.assertEquals(keys.Key._guessStringType(keydata.publicRSA_lsh), + 'public_lsh') + self.assertEquals(keys.Key._guessStringType(keydata.publicDSA_lsh), + 'public_lsh') + self.assertEquals(keys.Key._guessStringType(keydata.privateRSA_lsh), + 'private_lsh') + self.assertEquals(keys.Key._guessStringType(keydata.privateDSA_lsh), + 'private_lsh') + self.assertEquals(keys.Key._guessStringType( + keydata.privateRSA_agentv3), 'agentv3') + self.assertEquals(keys.Key._guessStringType( + keydata.privateDSA_agentv3), 'agentv3') + self.assertEquals(keys.Key._guessStringType( + '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01\x01'), + 'blob') + self.assertEquals(keys.Key._guessStringType( + '\x00\x00\x00\x07ssh-dss\x00\x00\x00\x01\x01'), + 'blob') + self.assertEquals(keys.Key._guessStringType('not a key'), + None) + + def _testPublicPrivateFromString(self, public, private, type, data): + self._testPublicFromString(public, type, data) + self._testPrivateFromString(private, type, data) + + def _testPublicFromString(self, public, type, data): + publicKey = keys.Key.fromString(public) + self.assertTrue(publicKey.isPublic()) + self.assertEquals(publicKey.type(), type) + for k, v in publicKey.data().items(): + self.assertEquals(data[k], v) + + def _testPrivateFromString(self, private, type, data): + privateKey = keys.Key.fromString(private) + self.assertFalse(privateKey.isPublic()) + self.assertEquals(privateKey.type(), type) + for k, v in data.items(): + self.assertEquals(privateKey.data()[k], v) + + def test_fromOpenSSH(self): + """ + Test that keys are correctly generated from OpenSSH strings. + """ + self._testPublicPrivateFromString(keydata.publicRSA_openssh, + keydata.privateRSA_openssh, 'RSA', keydata.RSAData) + self.assertEquals(keys.Key.fromString( + keydata.privateRSA_openssh_encrypted, + passphrase='encrypted'), + keys.Key.fromString(keydata.privateRSA_openssh)) + self.assertEquals(keys.Key.fromString( + keydata.privateRSA_openssh_alternate), + keys.Key.fromString(keydata.privateRSA_openssh)) + self._testPublicPrivateFromString(keydata.publicDSA_openssh, + keydata.privateDSA_openssh, 'DSA', keydata.DSAData) + + def test_fromLSH(self): + """ + Test that keys are correctly generated from LSH strings. + """ + self._testPublicPrivateFromString(keydata.publicRSA_lsh, + keydata.privateRSA_lsh, 'RSA', keydata.RSAData) + self._testPublicPrivateFromString(keydata.publicDSA_lsh, + keydata.privateDSA_lsh, 'DSA', keydata.DSAData) + sexp = sexpy.pack([['public-key', ['bad-key', ['p', '2']]]]) + self.assertRaises(keys.BadKeyError, keys.Key.fromString, + data='{'+base64.encodestring(sexp)+'}') + sexp = sexpy.pack([['private-key', ['bad-key', ['p', '2']]]]) + self.assertRaises(keys.BadKeyError, keys.Key.fromString, + sexp) + + def test_fromAgentv3(self): + """ + Test that keys are correctly generated from Agent v3 strings. + """ + self._testPrivateFromString(keydata.privateRSA_agentv3, 'RSA', + keydata.RSAData) + self._testPrivateFromString(keydata.privateDSA_agentv3, 'DSA', + keydata.DSAData) + self.assertRaises(keys.BadKeyError, keys.Key.fromString, + '\x00\x00\x00\x07ssh-foo'+'\x00\x00\x00\x01\x01'*5) + + def test_fromStringErrors(self): + """ + Test that fromString raises errors appropriately. + """ + self.assertRaises(keys.BadKeyError, keys.Key.fromString, '') + self.assertRaises(keys.BadKeyError, keys.Key.fromString, '', + 'bad_type') + self.assertRaises(keys.BadKeyError, keys.Key.fromString, + keydata.publicRSA_lsh, passphrase = 'unencrypted') + self.assertRaises(keys.EncryptedKeyError, keys.Key.fromString, + keys.Key(self.rsaObj).toString('openssh', 'encrypted')) + self.assertRaises(keys.BadKeyError, keys.Key.fromString, + '-----BEGIN RSA KEY-----\nwA==\n') + + def test_fromFile(self): + """ + Test that fromFile works correctly. + """ + self.assertEquals(keys.Key.fromFile(self.keyFile), + keys.Key.fromString(keydata.privateRSA_lsh)) + self.assertRaises(keys.BadKeyError, keys.Key.fromFile, + self.keyFile, 'bad_type') + self.assertRaises(keys.BadKeyError, keys.Key.fromFile, + self.keyFile, passphrase='unencrypted') + + def test_init(self): + """ + Test that the PublicKey object is initialized correctly. + """ + obj = Crypto.PublicKey.RSA.construct((1L, 2L)) + key = keys.Key(obj) + self.assertEquals(key.keyObject, obj) + + def test_equal(self): + """ + Test that Key objects are compared correctly. + """ + rsa1 = keys.Key(self.rsaObj) + rsa2 = keys.Key(self.rsaObj) + rsa3 = keys.Key(Crypto.PublicKey.RSA.construct((1L, 2L))) + dsa = keys.Key(self.dsaObj) + self.assertTrue(rsa1 == rsa2) + self.assertFalse(rsa1 == rsa3) + self.assertFalse(rsa1 == dsa) + self.assertFalse(rsa1 == object) + self.assertFalse(rsa1 == None) + + def test_notEqual(self): + """ + Test that Key objects are not-compared correctly. + """ + rsa1 = keys.Key(self.rsaObj) + rsa2 = keys.Key(self.rsaObj) + rsa3 = keys.Key(Crypto.PublicKey.RSA.construct((1L, 2L))) + dsa = keys.Key(self.dsaObj) + self.assertFalse(rsa1 != rsa2) + self.assertTrue(rsa1 != rsa3) + self.assertTrue(rsa1 != dsa) + self.assertTrue(rsa1 != object) + self.assertTrue(rsa1 != None) + + def test_type(self): + """ + Test that the type method returns the correct type for an object. + """ + self.assertEquals(keys.Key(self.rsaObj).type(), 'RSA') + self.assertEquals(keys.Key(self.rsaObj).sshType(), 'ssh-rsa') + self.assertEquals(keys.Key(self.dsaObj).type(), 'DSA') + self.assertEquals(keys.Key(self.dsaObj).sshType(), 'ssh-dss') + self.assertRaises(RuntimeError, keys.Key(None).type) + self.assertRaises(RuntimeError, keys.Key(None).sshType) + self.assertRaises(RuntimeError, keys.Key(self).type) + self.assertRaises(RuntimeError, keys.Key(self).sshType) + + def test_fromBlob(self): + """ + Test that a public key is correctly generated from a public key blob. + """ + rsaBlob = common.NS('ssh-rsa') + common.MP(2) + common.MP(3) + rsaKey = keys.Key.fromString(rsaBlob) + dsaBlob = (common.NS('ssh-dss') + common.MP(2) + common.MP(3) + + common.MP(4) + common.MP(5)) + dsaKey = keys.Key.fromString(dsaBlob) + badBlob = common.NS('ssh-bad') + self.assertTrue(rsaKey.isPublic()) + self.assertEquals(rsaKey.data(), {'e':2L, 'n':3L}) + self.assertTrue(dsaKey.isPublic()) + self.assertEquals(dsaKey.data(), {'p':2L, 'q':3L, 'g':4L, 'y':5L}) + self.assertRaises(keys.BadKeyError, + keys.Key.fromString, badBlob) + + + def test_fromPrivateBlob(self): + """ + Test that a private key is correctly generated from a private key blob. + """ + rsaBlob = (common.NS('ssh-rsa') + common.MP(2) + common.MP(3) + + common.MP(4) + common.MP(5) + common.MP(6) + common.MP(7)) + rsaKey = keys.Key._fromString_PRIVATE_BLOB(rsaBlob) + dsaBlob = (common.NS('ssh-dss') + common.MP(2) + common.MP(3) + + common.MP(4) + common.MP(5) + common.MP(6)) + dsaKey = keys.Key._fromString_PRIVATE_BLOB(dsaBlob) + badBlob = common.NS('ssh-bad') + self.assertFalse(rsaKey.isPublic()) + self.assertEqual( + rsaKey.data(), {'n':2L, 'e':3L, 'd':4L, 'u':5L, 'p':6L, 'q':7L}) + self.assertFalse(dsaKey.isPublic()) + self.assertEqual(dsaKey.data(), {'p':2L, 'q':3L, 'g':4L, 'y':5L, 'x':6L}) + self.assertRaises( + keys.BadKeyError, keys.Key._fromString_PRIVATE_BLOB, badBlob) + + + def test_blob(self): + """ + Test that the Key object generates blobs correctly. + """ + self.assertEquals(keys.Key(self.rsaObj).blob(), + '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01\x02' + '\x00\x00\x00\x01\x01') + self.assertEquals(keys.Key(self.dsaObj).blob(), + '\x00\x00\x00\x07ssh-dss\x00\x00\x00\x01\x03' + '\x00\x00\x00\x01\x04\x00\x00\x00\x01\x02' + '\x00\x00\x00\x01\x01') + + badKey = keys.Key(None) + self.assertRaises(RuntimeError, badKey.blob) + + + def test_privateBlob(self): + """ + L{Key.privateBlob} returns the SSH protocol-level format of the private + key and raises L{RuntimeError} if the underlying key object is invalid. + """ + self.assertEquals(keys.Key(self.rsaObj).privateBlob(), + '\x00\x00\x00\x07ssh-rsa\x00\x00\x00\x01\x01' + '\x00\x00\x00\x01\x02\x00\x00\x00\x01\x03\x00' + '\x00\x00\x01\x04\x00\x00\x00\x01\x04\x00\x00' + '\x00\x01\x05') + self.assertEquals(keys.Key(self.dsaObj).privateBlob(), + '\x00\x00\x00\x07ssh-dss\x00\x00\x00\x01\x03' + '\x00\x00\x00\x01\x04\x00\x00\x00\x01\x02\x00' + '\x00\x00\x01\x01\x00\x00\x00\x01\x05') + + badKey = keys.Key(None) + self.assertRaises(RuntimeError, badKey.privateBlob) + + + def test_toOpenSSH(self): + """ + Test that the Key object generates OpenSSH keys correctly. + """ + key = keys.Key.fromString(keydata.privateRSA_lsh) + self.assertEquals(key.toString('openssh'), keydata.privateRSA_openssh) + self.assertEquals(key.toString('openssh', 'encrypted'), + keydata.privateRSA_openssh_encrypted) + self.assertEquals(key.public().toString('openssh'), + keydata.publicRSA_openssh[:-8]) # no comment + self.assertEquals(key.public().toString('openssh', 'comment'), + keydata.publicRSA_openssh) + key = keys.Key.fromString(keydata.privateDSA_lsh) + self.assertEquals(key.toString('openssh'), keydata.privateDSA_openssh) + self.assertEquals(key.public().toString('openssh', 'comment'), + keydata.publicDSA_openssh) + self.assertEquals(key.public().toString('openssh'), + keydata.publicDSA_openssh[:-8]) # no comment + + def test_toLSH(self): + """ + Test that the Key object generates LSH keys correctly. + """ + key = keys.Key.fromString(keydata.privateRSA_openssh) + self.assertEquals(key.toString('lsh'), keydata.privateRSA_lsh) + self.assertEquals(key.public().toString('lsh'), + keydata.publicRSA_lsh) + key = keys.Key.fromString(keydata.privateDSA_openssh) + self.assertEquals(key.toString('lsh'), keydata.privateDSA_lsh) + self.assertEquals(key.public().toString('lsh'), + keydata.publicDSA_lsh) + + def test_toAgentv3(self): + """ + Test that the Key object generates Agent v3 keys correctly. + """ + key = keys.Key.fromString(keydata.privateRSA_openssh) + self.assertEquals(key.toString('agentv3'), keydata.privateRSA_agentv3) + key = keys.Key.fromString(keydata.privateDSA_openssh) + self.assertEquals(key.toString('agentv3'), keydata.privateDSA_agentv3) + + def test_toStringErrors(self): + """ + Test that toString raises errors appropriately. + """ + self.assertRaises(keys.BadKeyError, keys.Key(self.rsaObj).toString, + 'bad_type') + + def test_sign(self): + """ + Test that the Key object generates correct signatures. + """ + key = keys.Key.fromString(keydata.privateRSA_openssh) + self.assertEquals(key.sign(''), self.rsaSignature) + key = keys.Key.fromString(keydata.privateDSA_openssh) + self.assertEquals(key.sign(''), self.dsaSignature) + + + def test_verify(self): + """ + Test that the Key object correctly verifies signatures. + """ + key = keys.Key.fromString(keydata.publicRSA_openssh) + self.assertTrue(key.verify(self.rsaSignature, '')) + self.assertFalse(key.verify(self.rsaSignature, 'a')) + self.assertFalse(key.verify(self.dsaSignature, '')) + key = keys.Key.fromString(keydata.publicDSA_openssh) + self.assertTrue(key.verify(self.dsaSignature, '')) + self.assertFalse(key.verify(self.dsaSignature, 'a')) + self.assertFalse(key.verify(self.rsaSignature, '')) + + def test_repr(self): + """ + Test the pretty representation of Key. + """ + self.assertEquals(repr(keys.Key(self.rsaObj)), +"""""") + +class WarningsTestCase(unittest.TestCase): + """ + Test that deprecated functions warn the user of their deprecation. + """ + if Crypto is None: + skip = "cannot run w/o PyCrypto" + + def setUp(self): + self.keyObject = keys.Key.fromString(keydata.privateRSA_lsh).keyObject + + def test_getPublicKeyString(self): + """ + Test that getPublicKeyString warns with a DeprecationWarning. + """ + self.assertWarns(DeprecationWarning, + "getPublicKeyString is deprecated since Twisted Conch 0.9." + " Use Key.fromString().", + unittest.__file__, keys.getPublicKeyString, + data=keydata.publicRSA_openssh) + + def test_makePublicKeyString(self): + """ + Test that makePublicKeyString warns with a DeprecationWarning. + """ + self.assertWarns(DeprecationWarning, + "makePublicKeyString is deprecated since Twisted Conch 0.9." + " Use Key(obj).toString().", unittest.__file__, + keys.makePublicKeyString, self.keyObject) + + def test_getPublicKeyObject(self): + """ + Test that getPublicKeyObject warns with a DeprecationWarning. + """ + self.assertWarns(DeprecationWarning, + "getPublicKeyObject is deprecated since Twisted Conch 0.9." + " Use Key.fromString().", unittest.__file__, + keys.getPublicKeyObject, keydata.publicRSA_lsh) + + def test_getPrivateKeyObject(self): + """ + Test that getPrivateKeyObject warns with a DeprecationWarning. + """ + self.assertWarns(DeprecationWarning, + "getPrivateKeyObject is deprecated since Twisted Conch 0.9." + " Use Key.fromString().", unittest.__file__, + keys.getPrivateKeyObject, data=keydata.privateRSA_lsh) + + def test_makePrivateKeyString(self): + """ + Test that makePrivateKeyString warns with a DeprecationWarning. + """ + self.assertWarns(DeprecationWarning, + "makePrivateKeyString is deprecated since Twisted Conch 0.9." + " Use Key(obj).toString().", unittest.__file__, + keys.makePrivateKeyString, self.keyObject) + + def test_makePublicKeyBlob(self): + """ + Test that makePublicKeyBlob warns with a DeprecationWarning. + """ + self.assertWarns(DeprecationWarning, + "makePublicKeyBlob is deprecated since Twisted Conch 0.9." + " Use Key(obj).blob().", unittest.__file__, + keys.makePublicKeyBlob, self.keyObject) + + def test_signData(self): + """ + Test that signData warns with a DeprecationWarning. + """ + self.assertWarns(DeprecationWarning, + "signData is deprecated since Twisted Conch 0.9." + " Use Key(obj).sign(data).", unittest.__file__, + keys.signData, self.keyObject, '') + + def test_verifySignature(self): + """ + Test that signData warns with a DeprecationWarning. + """ + self.assertWarns(DeprecationWarning, + "verifySignature is deprecated since Twisted Conch 0.9." + " Use Key(obj).verify(signature, data).", unittest.__file__, + keys.verifySignature, self.keyObject, '\x00\x00\x00\x00', '') + + def test_printKey(self): + """ + Test that signData warns with a DeprecationWarning. + """ + self.assertWarns(DeprecationWarning, + "printKey is deprecated since Twisted Conch 0.9." + " Use repr(Key(obj)).", unittest.__file__, + keys.printKey, self.keyObject) - def _testKeyFromString(self, privKey, pubKey, privData, pubData): - keyType = keys.objectType(privKey) - privFS = keys.getPrivateKeyObject(data = privData) - pubFS = keys.getPublicKeyObject(keys.getPublicKeyString(data=pubData)) - for k in privFS.keydata: - if getattr(privFS, k) != getattr(privKey, k): - self.fail('getting %s private key from string failed' % keyType) - for k in pubFS.keydata: - if hasattr(pubFS, k): - if getattr(pubFS, k) != getattr(pubKey, k): - self.fail('getting %s public key from string failed' % keyType) - - def _testGenerateKey(self, privKey, pubKey, privData, pubData, keyType): - self.assertEquals(keys.makePublicKeyString(pubKey, 'comment', keyType), pubData) - self.assertEquals(keys.makePublicKeyString(privKey, 'comment', keyType), pubData) - self.assertEquals(keys.makePrivateKeyString(privKey, kind=keyType), privData) - if keyType != 'lsh': - encData = keys.makePrivateKeyString(privKey, passphrase='test', kind=keyType) - self.assertEquals( - keys.getPrivateKeyObject(data = encData, - passphrase = 'test').__getstate__(), - privKey.__getstate__()) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_knownhosts.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_knownhosts.py --- twisted-conch-0.8.0/twisted/conch/test/test_knownhosts.py 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_knownhosts.py 2008-11-16 02:54:52.000000000 +0000 @@ -0,0 +1,983 @@ +# Copyright (c) 2008 Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Tests for L{twisted.conch.client.knownhosts}. +""" + +import os + +from zope.interface.verify import verifyObject + +from twisted.python.filepath import FilePath + +from twisted.trial.unittest import TestCase + +from twisted.internet.defer import Deferred + +from twisted.conch.interfaces import IKnownHostEntry +from twisted.conch.error import HostKeyChanged, UserRejectedKey, InvalidEntry +from twisted.conch.ssh.keys import Key, BadKeyError +from twisted.conch.client.knownhosts import \ + PlainEntry, HashedEntry, KnownHostsFile, UnparsedEntry, ConsoleUI +from binascii import Error as BinasciiError, b2a_base64, a2b_base64 + +from twisted.conch.client import default + +sampleEncodedKey = ( + 'AAAAB3NzaC1yc2EAAAABIwAAAQEAsV0VMRbGmzhqxxayLRHmvnFvtyNqgbNKV46dU1bVFB+3y' + 'tNvue4Riqv/SVkPRNwMb7eWH29SviXaBxUhYyzKkDoNUq3rTNnH1Vnif6d6X4JCrUb5d3W+Dm' + 'YClyJrZ5HgD/hUpdSkTRqdbQ2TrvSAxRacj+vHHT4F4dm1bJSewm3B2D8HVOoi/CbVh3dsIiC' + 'dp8VltdZx4qYVfYe2LwVINCbAa3d3tj9ma7RVfw3OH2Mfb+toLd1N5tBQFb7oqTt2nC6I/6Bd' + '4JwPUld+IEitw/suElq/AIJVQXXujeyiZlea90HE65U2mF1ytr17HTAIT2ySokJWyuBANGACk' + '6iIaw==') + +otherSampleEncodedKey = ( + 'AAAAB3NzaC1yc2EAAAABIwAAAIEAwaeCZd3UCuPXhX39+/p9qO028jTF76DMVd9mPvYVDVXuf' + 'WckKZauF7+0b7qm+ChT7kan6BzRVo4++gCVNfAlMzLysSt3ylmOR48tFpAfygg9UCX3DjHz0E' + 'lOOUKh3iifc9aUShD0OPaK3pR5JJ8jfiBfzSYWt/hDi/iZ4igsSs8=') + +thirdSampleEncodedKey = ( + 'AAAAB3NzaC1yc2EAAAABIwAAAQEAl/TQakPkePlnwCBRPitIVUTg6Z8VzN1en+DGkyo/evkmLw' + '7o4NWR5qbysk9A9jXW332nxnEuAnbcCam9SHe1su1liVfyIK0+3bdn0YRB0sXIbNEtMs2LtCho' + '/aV3cXPS+Cf1yut3wvIpaRnAzXxuKPCTXQ7/y0IXa8TwkRBH58OJa3RqfQ/NsSp5SAfdsrHyH2' + 'aitiVKm2jfbTKzSEqOQG/zq4J9GXTkq61gZugory/Tvl5/yPgSnOR6C9jVOMHf27ZPoRtyj9SY' + '343Hd2QHiIE0KPZJEgCynKeWoKz8v6eTSK8n4rBnaqWdp8MnGZK1WGy05MguXbyCDuTC8AmJXQ' + '==') + +sampleKey = a2b_base64(sampleEncodedKey) +otherSampleKey = a2b_base64(otherSampleEncodedKey) +thirdSampleKey = a2b_base64(thirdSampleEncodedKey) + +samplePlaintextLine = ( + "www.twistedmatrix.com ssh-rsa " + sampleEncodedKey + "\n") + +otherSamplePlaintextLine = ( + "divmod.com ssh-rsa " + otherSampleEncodedKey + "\n") + +sampleHostIPLine = ( + "www.twistedmatrix.com,198.49.126.131 ssh-rsa " + sampleEncodedKey + "\n") + +sampleHashedLine = ( + "|1|gJbSEPBG9ZSBoZpHNtZBD1bHKBA=|bQv+0Xa0dByrwkA1EB0E7Xop/Fo= ssh-rsa " + + sampleEncodedKey + "\n") + + + +class EntryTestsMixin: + """ + Tests for implementations of L{IKnownHostEntry}. Subclasses must set the + 'entry' attribute to a provider of that interface, the implementation of + that interface under test. + + @ivar entry: a provider of L{IKnownHostEntry} with a hostname of + www.twistedmatrix.com and an RSA key of sampleKey. + """ + + def test_providesInterface(self): + """ + The given entry should provide IKnownHostEntry. + """ + verifyObject(IKnownHostEntry, self.entry) + + + def test_fromString(self): + """ + Constructing a plain text entry from an unhashed known_hosts entry will + result in an L{IKnownHostEntry} provider with 'keyString', 'hostname', + and 'keyType' attributes. While outside the interface in question, + these attributes are held in common by L{PlainEntry} and L{HashedEntry} + implementations; other implementations should override this method in + subclasses. + """ + entry = self.entry + self.assertEqual(entry.publicKey, Key.fromString(sampleKey)) + self.assertEqual(entry.keyType, "ssh-rsa") + + + def test_matchesKey(self): + """ + L{IKnownHostEntry.matchesKey} checks to see if an entry matches a given + SSH key. + """ + twistedmatrixDotCom = Key.fromString(sampleKey) + divmodDotCom = Key.fromString(otherSampleKey) + self.assertEqual( + True, + self.entry.matchesKey(twistedmatrixDotCom)) + self.assertEqual( + False, + self.entry.matchesKey(divmodDotCom)) + + + def test_matchesHost(self): + """ + L{IKnownHostEntry.matchesHost} checks to see if an entry matches a + given hostname. + """ + self.assertEqual(True, self.entry.matchesHost( + "www.twistedmatrix.com")) + self.assertEqual(False, self.entry.matchesHost( + "www.divmod.com")) + + + +class PlainEntryTests(EntryTestsMixin, TestCase): + """ + Test cases for L{PlainEntry}. + """ + plaintextLine = samplePlaintextLine + hostIPLine = sampleHostIPLine + + def setUp(self): + """ + Set 'entry' to a sample plain-text entry with sampleKey as its key. + """ + self.entry = PlainEntry.fromString(self.plaintextLine) + + + def test_matchesHostIP(self): + """ + A "hostname,ip" formatted line will match both the host and the IP. + """ + self.entry = PlainEntry.fromString(self.hostIPLine) + self.assertEqual(True, self.entry.matchesHost("198.49.126.131")) + self.test_matchesHost() + + + def test_toString(self): + """ + L{PlainEntry.toString} generates the serialized OpenSSL format string + for the entry, sans newline. + """ + self.assertEqual(self.entry.toString(), self.plaintextLine.rstrip("\n")) + multiHostEntry = PlainEntry.fromString(self.hostIPLine) + self.assertEqual(multiHostEntry.toString(), self.hostIPLine.rstrip("\n")) + + + +class PlainTextWithCommentTests(PlainEntryTests): + """ + Test cases for L{PlainEntry} when parsed from a line with a comment. + """ + + plaintextLine = samplePlaintextLine[:-1] + " plain text comment.\n" + hostIPLine = sampleHostIPLine[:-1] + " text following host/IP line\n" + + + +class HashedEntryTests(EntryTestsMixin, TestCase): + """ + Tests for L{HashedEntry}. + + This suite doesn't include any tests for host/IP pairs because hashed + entries store IP addresses the same way as hostnames and does not support + comma-separated lists. (If you hash the IP and host together you can't + tell if you've got the key already for one or the other.) + """ + hashedLine = sampleHashedLine + + def setUp(self): + """ + Set 'entry' to a sample hashed entry for twistedmatrix.com with + sampleKey as its key. + """ + self.entry = HashedEntry.fromString(self.hashedLine) + + + def test_toString(self): + """ + L{HashedEntry.toString} generates the serialized OpenSSL format string + for the entry, sans the newline. + """ + self.assertEqual(self.entry.toString(), self.hashedLine.rstrip("\n")) + + + +class HashedEntryWithCommentTests(HashedEntryTests): + """ + Test cases for L{PlainEntry} when parsed from a line with a comment. + """ + + hashedLine = sampleHashedLine[:-1] + " plain text comment.\n" + + + +class UnparsedEntryTests(TestCase, EntryTestsMixin): + """ + Tests for L{UnparsedEntry} + """ + def setUp(self): + """ + Set up the 'entry' to be an unparsed entry for some random text. + """ + self.entry = UnparsedEntry(" This is a bogus entry. \n") + + + def test_fromString(self): + """ + Creating an L{UnparsedEntry} should simply record the string it was + passed. + """ + self.assertEqual(" This is a bogus entry. \n", + self.entry._string) + + + def test_matchesHost(self): + """ + An unparsed entry can't match any hosts. + """ + self.assertEqual(False, self.entry.matchesHost("www.twistedmatrix.com")) + + + def test_matchesKey(self): + """ + An unparsed entry can't match any keys. + """ + self.assertEqual(False, self.entry.matchesKey(Key.fromString(sampleKey))) + + + def test_toString(self): + """ + L{UnparsedEntry.toString} returns its input string, sans trailing newline. + """ + self.assertEqual(" This is a bogus entry. ", self.entry.toString()) + + + +class ParseErrorTests(TestCase): + """ + L{HashedEntry.fromString} and L{PlainEntry.fromString} can raise a variety + of errors depending on misformattings of certain strings. These tests make + sure those errors are caught. Since many of the ways that this can go + wrong are in the lower-level APIs being invoked by the parsing logic, + several of these are integration tests with the L{base64} and + L{twisted.conch.ssh.keys} modules. + """ + + def invalidEntryTest(self, cls): + """ + If there are fewer than three elements, C{fromString} should raise + L{InvalidEntry}. + """ + self.assertRaises(InvalidEntry, cls.fromString, "invalid") + + + def notBase64Test(self, cls): + """ + If the key is not base64, C{fromString} should raise L{BinasciiError}. + """ + self.assertRaises(BinasciiError, cls.fromString, "x x x") + + + def badKeyTest(self, cls, prefix): + """ + If the key portion of the entry is valid base64, but is not actually an + SSH key, C{fromString} should raise L{BadKeyError}. + """ + self.assertRaises(BadKeyError, cls.fromString, ' '.join( + [prefix, "ssh-rsa", b2a_base64( + "Hey, this isn't an SSH key!").strip()])) + + + def test_invalidPlainEntry(self): + """ + If there are fewer than three whitespace-separated elements in an + entry, L{PlainEntry.fromString} should raise L{InvalidEntry}. + """ + self.invalidEntryTest(PlainEntry) + + + def test_invalidHashedEntry(self): + """ + If there are fewer than three whitespace-separated elements in an + entry, or the hostname salt/hash portion has more than two elements, + L{HashedEntry.fromString} should raise L{InvalidEntry}. + """ + self.invalidEntryTest(HashedEntry) + a, b, c = sampleHashedLine.split() + self.assertRaises(InvalidEntry, HashedEntry.fromString, ' '.join( + [a + "||", b, c])) + + + def test_plainNotBase64(self): + """ + If the key portion of a plain entry is not decodable as base64, + C{fromString} should raise L{BinasciiError}. + """ + self.notBase64Test(PlainEntry) + + + def test_hashedNotBase64(self): + """ + If the key, host salt, or host hash portion of a hashed entry is not + encoded, it will raise L{BinasciiError}. + """ + self.notBase64Test(HashedEntry) + a, b, c = sampleHashedLine.split() + # Salt not valid base64. + self.assertRaises( + BinasciiError, HashedEntry.fromString, + ' '.join(["|1|x|" + b2a_base64("stuff").strip(), b, c])) + # Host hash not valid base64. + self.assertRaises( + BinasciiError, HashedEntry.fromString, + ' '.join([HashedEntry.MAGIC + b2a_base64("stuff").strip() + "|x", b, c])) + # Neither salt nor hash valid base64. + self.assertRaises( + BinasciiError, HashedEntry.fromString, + ' '.join(["|1|x|x", b, c])) + + + def test_hashedBadKey(self): + """ + If the key portion of the entry is valid base64, but is not actually an + SSH key, C{HashedEntry.fromString} should raise L{BadKeyError}. + """ + a, b, c = sampleHashedLine.split() + self.badKeyTest(HashedEntry, a) + + + def test_plainBadKey(self): + """ + If the key portion of the entry is valid base64, but is not actually an + SSH key, C{PlainEntry.fromString} should raise L{BadKeyError}. + """ + self.badKeyTest(PlainEntry, "hostname") + + + +class KnownHostsDatabaseTests(TestCase): + """ + Tests for L{KnownHostsFile}. + """ + + def pathWithContent(self, content): + """ + Return a FilePath with the given initial content. + """ + fp = FilePath(self.mktemp()) + fp.setContent(content) + return fp + + + def loadSampleHostsFile(self, content=( + sampleHashedLine + otherSamplePlaintextLine + + "\n# That was a blank line.\n" + "This is just unparseable.\n" + "This also unparseable.\n")): + """ + Return a sample hosts file, with keys for www.twistedmatrix.com and + divmod.com present. + """ + return KnownHostsFile.fromPath(self.pathWithContent(content)) + + + def test_loadFromPath(self): + """ + Loading a L{KnownHostsFile} from a path with six entries in it will + result in a L{KnownHostsFile} object with six L{IKnownHostEntry} + providers in it, each of the appropriate type. + """ + hostsFile = self.loadSampleHostsFile() + self.assertEqual(len(hostsFile._entries), 6) + self.assertIsInstance(hostsFile._entries[0], HashedEntry) + self.assertEqual(True, hostsFile._entries[0].matchesHost( + "www.twistedmatrix.com")) + self.assertIsInstance(hostsFile._entries[1], PlainEntry) + self.assertEqual(True, hostsFile._entries[1].matchesHost( + "divmod.com")) + self.assertIsInstance(hostsFile._entries[2], UnparsedEntry) + self.assertEqual(hostsFile._entries[2].toString(), "") + self.assertIsInstance(hostsFile._entries[3], UnparsedEntry) + self.assertEqual(hostsFile._entries[3].toString(), + "# That was a blank line.") + self.assertIsInstance(hostsFile._entries[4], UnparsedEntry) + self.assertEqual(hostsFile._entries[4].toString(), + "This is just unparseable.") + self.assertIsInstance(hostsFile._entries[5], UnparsedEntry) + self.assertEqual(hostsFile._entries[5].toString(), + "This also unparseable.") + + + def test_loadNonExistent(self): + """ + Loading a L{KnownHostsFile} from a path that does not exist should + result in an empty L{KnownHostsFile} that will save back to that path. + """ + pn = self.mktemp() + knownHostsFile = KnownHostsFile.fromPath(FilePath(pn)) + self.assertEqual([], list(knownHostsFile._entries)) + self.assertEqual(False, FilePath(pn).exists()) + knownHostsFile.save() + self.assertEqual(True, FilePath(pn).exists()) + + + def test_loadNonExistentParent(self): + """ + Loading a L{KnownHostsFile} from a path whose parent directory does not + exist should result in an empty L{KnownHostsFile} that will save back + to that path, creating its parent directory(ies) in the process. + """ + thePath = FilePath(self.mktemp()) + knownHostsPath = thePath.child("foo").child("known_hosts") + knownHostsFile = KnownHostsFile.fromPath(knownHostsPath) + knownHostsFile.save() + knownHostsPath.restat(False) + self.assertEqual(True, knownHostsPath.exists()) + + + def test_savingAddsEntry(self): + """ + L{KnownHostsFile.save()} will write out a new file with any entries + that have been added. + """ + path = self.pathWithContent(sampleHashedLine + + otherSamplePlaintextLine) + knownHostsFile = KnownHostsFile.fromPath(path) + newEntry = knownHostsFile.addHostKey("some.example.com", Key.fromString(thirdSampleKey)) + expectedContent = ( + sampleHashedLine + + otherSamplePlaintextLine + HashedEntry.MAGIC + + b2a_base64(newEntry._hostSalt).strip() + "|" + + b2a_base64(newEntry._hostHash).strip() + " ssh-rsa " + + thirdSampleEncodedKey + "\n") + + # Sanity check, let's make sure the base64 API being used for the test + # isn't inserting spurious newlines. + self.assertEqual(3, expectedContent.count("\n")) + knownHostsFile.save() + self.assertEqual(expectedContent, path.getContent()) + + + def test_hasPresentKey(self): + """ + L{KnownHostsFile.hasHostKey} returns C{True} when a key for the given + hostname is present and matches the expected key. + """ + hostsFile = self.loadSampleHostsFile() + self.assertEqual(True, hostsFile.hasHostKey( + "www.twistedmatrix.com", Key.fromString(sampleKey))) + + + def test_hasNonPresentKey(self): + """ + L{KnownHostsFile.hasHostKey} returns C{False} when a key for the given + hostname is not present. + """ + hostsFile = self.loadSampleHostsFile() + self.assertEqual(False, hostsFile.hasHostKey( + "non-existent.example.com", Key.fromString(sampleKey))) + + + def test_hasKeyMismatch(self): + """ + L{KnownHostsFile.hasHostKey} raises L{HostKeyChanged} if the host key + is present, but different from the expected one. The resulting + exception should have an offendingEntry indicating the given entry. + """ + hostsFile = self.loadSampleHostsFile() + exception = self.assertRaises( + HostKeyChanged, hostsFile.hasHostKey, + "www.twistedmatrix.com", Key.fromString(otherSampleKey)) + self.assertEqual(exception.offendingEntry, hostsFile._entries[0]) + self.assertEqual(exception.lineno, 1) + self.assertEqual(exception.path, hostsFile._savePath) + + + def test_addHostKey(self): + """ + L{KnownHostsFile.addHostKey} adds a new L{HashedEntry} to the host + file, and returns it. + """ + hostsFile = self.loadSampleHostsFile() + aKey = Key.fromString(thirdSampleKey) + self.assertEqual(False, + hostsFile.hasHostKey("somewhere.example.com", aKey)) + newEntry = hostsFile.addHostKey("somewhere.example.com", aKey) + + # The code in OpenSSH requires host salts to be 20 characters long. + # This is the required length of a SHA-1 HMAC hash, so it's just a + # sanity check. + self.assertEqual(20, len(newEntry._hostSalt)) + self.assertEqual(True, + newEntry.matchesHost("somewhere.example.com")) + self.assertEqual(newEntry.keyType, "ssh-rsa") + self.assertEqual(aKey, newEntry.publicKey) + self.assertEqual(True, + hostsFile.hasHostKey("somewhere.example.com", aKey)) + + + def test_randomSalts(self): + """ + L{KnownHostsFile.addHostKey} generates a random salt for each new key, + so subsequent salts will be different. + """ + hostsFile = self.loadSampleHostsFile() + aKey = Key.fromString(thirdSampleKey) + self.assertNotEqual( + hostsFile.addHostKey("somewhere.example.com", aKey)._hostSalt, + hostsFile.addHostKey("somewhere-else.example.com", aKey)._hostSalt) + + + def test_verifyValidKey(self): + """ + Verifying a valid key should return a L{Deferred} which fires with + True. + """ + hostsFile = self.loadSampleHostsFile() + hostsFile.addHostKey("1.2.3.4", Key.fromString(sampleKey)) + ui = FakeUI() + d = hostsFile.verifyHostKey(ui, "www.twistedmatrix.com", "1.2.3.4", + Key.fromString(sampleKey)) + l = [] + d.addCallback(l.append) + self.assertEqual(l, [True]) + + + def test_verifyInvalidKey(self): + """ + Verfying an invalid key should return a L{Deferred} which fires with a + L{HostKeyChanged} failure. + """ + hostsFile = self.loadSampleHostsFile() + wrongKey = Key.fromString(thirdSampleKey) + ui = FakeUI() + hostsFile.addHostKey("1.2.3.4", Key.fromString(sampleKey)) + d = hostsFile.verifyHostKey( + ui, "www.twistedmatrix.com", "1.2.3.4", wrongKey) + l = [] + d.addErrback(l.append) + hkc = l[0].trap(HostKeyChanged) + + + def verifyNonPresentKey(self): + """ + Set up a test to verify a key that isn't present. Return a 3-tuple of + the UI, a list set up to collect the result of the verifyHostKey call, + and the sample L{KnownHostsFile} being used. + + This utility method avoids returning a L{Deferred}, and records results + in the returned list instead, because the events which get generated + here are pre-recorded in the 'ui' object. If the L{Deferred} in + question does not fire, the it will fail quickly with an empty list. + """ + hostsFile = self.loadSampleHostsFile() + absentKey = Key.fromString(thirdSampleKey) + ui = FakeUI() + l = [] + d = hostsFile.verifyHostKey( + ui, "sample-host.example.com", "4.3.2.1", absentKey) + d.addBoth(l.append) + self.assertEqual([], l) + self.assertEqual( + ui.promptText, + "The authenticity of host 'sample-host.example.com (4.3.2.1)' " + "can't be established.\n" + "RSA key fingerprint is " + "89:4e:cc:8c:57:83:96:48:ef:63:ad:ee:99:00:4c:8f.\n" + "Are you sure you want to continue connecting (yes/no)? ") + return ui, l, hostsFile + + + def test_verifyNonPresentKey_Yes(self): + """ + Verifying a key where neither the hostname nor the IP are present + should result in the UI being prompted with a message explaining as + much. If the UI says yes, the Deferred should fire with True. + """ + ui, l, knownHostsFile = self.verifyNonPresentKey() + ui.promptDeferred.callback(True) + self.assertEqual([True], l) + reloaded = KnownHostsFile.fromPath(knownHostsFile._savePath) + self.assertEqual( + True, + reloaded.hasHostKey("4.3.2.1", Key.fromString(thirdSampleKey))) + self.assertEqual( + True, + reloaded.hasHostKey("sample-host.example.com", + Key.fromString(thirdSampleKey))) + + + def test_verifyNonPresentKey_No(self): + """ + Verifying a key where neither the hostname nor the IP are present + should result in the UI being prompted with a message explaining as + much. If the UI says no, the Deferred should fail with + UserRejectedKey. + """ + ui, l, knownHostsFile = self.verifyNonPresentKey() + ui.promptDeferred.callback(False) + l[0].trap(UserRejectedKey) + + + def test_verifyHostIPMismatch(self): + """ + Verifying a key where the host is present (and correct), but the IP is + present and different, should result the deferred firing in a + HostKeyChanged failure. + """ + hostsFile = self.loadSampleHostsFile() + rightKey = Key.fromString(sampleKey) + wrongKey = Key.fromString(thirdSampleKey) + ui = FakeUI() + l = [] + d = hostsFile.verifyHostKey( + ui, "www.twistedmatrix.com", "4.3.2.1", wrongKey) + d.addErrback(l.append) + l[0].trap(HostKeyChanged) + + + def test_verifyKeyForHostAndIP(self): + """ + Verifying a key where the hostname is present but the IP is not should + result in the key being added for the IP and the user being warned + about the change. + """ + ui = FakeUI() + hostsFile = self.loadSampleHostsFile() + expectedKey = Key.fromString(sampleKey) + hostsFile.verifyHostKey( + ui, "www.twistedmatrix.com", "5.4.3.2", expectedKey) + self.assertEqual( + True, KnownHostsFile.fromPath(hostsFile._savePath).hasHostKey( + "5.4.3.2", expectedKey)) + self.assertEqual( + ["Warning: Permanently added the RSA host key for IP address " + "'5.4.3.2' to the list of known hosts."], + ui.userWarnings) + + +class FakeFile(object): + """ + A fake file-like object that acts enough like a file for + L{ConsoleUI.prompt}. + """ + + def __init__(self): + self.inlines = [] + self.outchunks = [] + self.closed = False + + + def readline(self): + """ + Return a line from the 'inlines' list. + """ + return self.inlines.pop(0) + + + def write(self, chunk): + """ + Append the given item to the 'outchunks' list. + """ + if self.closed: + raise IOError("the file was closed") + self.outchunks.append(chunk) + + + def close(self): + """ + Set the 'closed' flag to True, explicitly marking that it has been + closed. + """ + self.closed = True + + + +class ConsoleUITests(TestCase): + """ + Test cases for L{ConsoleUI}. + """ + + def setUp(self): + """ + Create a L{ConsoleUI} pointed at a L{FakeFile}. + """ + self.fakeFile = FakeFile() + self.ui = ConsoleUI(self.openFile) + + + def openFile(self): + """ + Return the current fake file. + """ + return self.fakeFile + + + def newFile(self, lines): + """ + Create a new fake file (the next file that self.ui will open) with the + given list of lines to be returned from readline(). + """ + self.fakeFile = FakeFile() + self.fakeFile.inlines = lines + + + def test_promptYes(self): + """ + L{ConsoleUI.prompt} writes a message to the console, then reads a line. + If that line is 'yes', then it returns a L{Deferred} that fires with + True. + """ + for okYes in ['yes', 'Yes', 'yes\n']: + self.newFile([okYes]) + l = [] + self.ui.prompt("Hello, world!").addCallback(l.append) + self.assertEqual(["Hello, world!"], self.fakeFile.outchunks) + self.assertEqual([True], l) + self.assertEqual(True, self.fakeFile.closed) + + + def test_promptNo(self): + """ + L{ConsoleUI.prompt} writes a message to the console, then reads a line. + If that line is 'no', then it returns a L{Deferred} that fires with + False. + """ + for okNo in ['no', 'No', 'no\n']: + self.newFile([okNo]) + l = [] + self.ui.prompt("Goodbye, world!").addCallback(l.append) + self.assertEqual(["Goodbye, world!"], self.fakeFile.outchunks) + self.assertEqual([False], l) + self.assertEqual(True, self.fakeFile.closed) + + + def test_promptRepeatedly(self): + """ + L{ConsoleUI.prompt} writes a message to the console, then reads a line. + If that line is neither 'yes' nor 'no', then it says "Please enter + 'yes' or 'no'" until it gets a 'yes' or a 'no', at which point it + returns a Deferred that answers either True or False. + """ + self.newFile(['what', 'uh', 'okay', 'yes']) + l = [] + self.ui.prompt("Please say something useful.").addCallback(l.append) + self.assertEqual([True], l) + self.assertEqual(self.fakeFile.outchunks, + ["Please say something useful."] + + ["Please type 'yes' or 'no': "] * 3) + self.assertEqual(True, self.fakeFile.closed) + self.newFile(['blah', 'stuff', 'feh', 'no']) + l = [] + self.ui.prompt("Please say something negative.").addCallback(l.append) + self.assertEqual([False], l) + self.assertEqual(self.fakeFile.outchunks, + ["Please say something negative."] + + ["Please type 'yes' or 'no': "] * 3) + self.assertEqual(True, self.fakeFile.closed) + + + def test_promptOpenFailed(self): + """ + If the C{opener} passed to L{ConsoleUI} raises an exception, that + exception will fail the L{Deferred} returned from L{ConsoleUI.prompt}. + """ + def raiseIt(): + raise IOError() + ui = ConsoleUI(raiseIt) + l = [] + ui.prompt("This is a test.").addErrback(l.append) + l[0].trap(IOError) + + + def test_warn(self): + """ + L{ConsoleUI.warn} should output a message to the console object. + """ + self.ui.warn("Test message.") + self.assertEqual(["Test message."], self.fakeFile.outchunks) + self.assertEqual(True, self.fakeFile.closed) + + + def test_warnOpenFailed(self): + """ + L{ConsoleUI.warn} should log a traceback if the output can't be opened. + """ + def raiseIt(): + 1 / 0 + ui = ConsoleUI(raiseIt) + ui.warn("This message never makes it.") + self.assertEqual(len(self.flushLoggedErrors(ZeroDivisionError)), 1) + + + +class FakeUI(object): + """ + A fake UI object, adhering to the interface expected by + L{KnownHostsFile.verifyHostKey} + + @ivar userWarnings: inputs provided to 'warn'. + + @ivar promptDeferred: last result returned from 'prompt'. + + @ivar promptText: the last input provided to 'prompt'. + """ + + def __init__(self): + self.userWarnings = [] + self.promptDeferred = None + self.promptText = None + + + def prompt(self, text): + """ + Issue the user an interactive prompt, which they can accept or deny. + """ + self.promptText = text + self.promptDeferred = Deferred() + return self.promptDeferred + + + def warn(self, text): + """ + Issue a non-interactive warning to the user. + """ + self.userWarnings.append(text) + + + +class FakeObject(object): + """ + A fake object that can have some attributes. Used to fake + L{SSHClientTransport} and L{SSHClientFactory}. + """ + + +class DefaultAPITests(TestCase): + """ + The API in L{twisted.conch.client.default.verifyHostKey} is the integration + point between the code in the rest of conch and L{KnownHostsFile}. + """ + + def patchedOpen(self, fname, mode): + """ + The patched version of 'open'; this returns a L{FakeFile} that the + instantiated L{ConsoleUI} can use. + """ + self.assertEqual(fname, "/dev/tty") + self.assertEqual(mode, "r+b") + return self.fakeFile + + + def setUp(self): + """ + Patch 'open' in verifyHostKey. + """ + self.fakeFile = FakeFile() + self.patch(default, "_open", self.patchedOpen) + self.hostsOption = self.mktemp() + knownHostsFile = KnownHostsFile(FilePath(self.hostsOption)) + knownHostsFile.addHostKey("exists.example.com", Key.fromString(sampleKey)) + knownHostsFile.addHostKey("4.3.2.1", Key.fromString(sampleKey)) + knownHostsFile.save() + self.fakeTransport = FakeObject() + self.fakeTransport.factory = FakeObject() + self.options = self.fakeTransport.factory.options = { + 'host': "exists.example.com", + 'known-hosts': self.hostsOption + } + + + def test_verifyOKKey(self): + """ + L{default.verifyHostKey} should return a L{Deferred} which fires with + C{1} when passed a host, IP, and key which already match the + known_hosts file it is supposed to check. + """ + l = [] + default.verifyHostKey(self.fakeTransport, "4.3.2.1", sampleKey, + "I don't care.").addCallback(l.append) + self.assertEqual([1], l) + + + def replaceHome(self, tempHome): + """ + Replace the HOME environment variable until the end of the current + test, with the given new home-directory, so that L{os.path.expanduser} + will yield controllable, predictable results. + + @param tempHome: the pathname to replace the HOME variable with. + + @type tempHome: L{str} + """ + oldHome = os.environ.get('HOME') + def cleanupHome(): + if oldHome is None: + del os.environ['HOME'] + else: + os.environ['HOME'] = oldHome + self.addCleanup(cleanupHome) + os.environ['HOME'] = tempHome + + + def test_noKnownHostsOption(self): + """ + L{default.verifyHostKey} should find your known_hosts file in + ~/.ssh/known_hosts if you don't specify one explicitly on the command + line. + """ + l = [] + tmpdir = self.mktemp() + oldHostsOption = self.hostsOption + hostsNonOption = FilePath(tmpdir).child(".ssh").child("known_hosts") + hostsNonOption.parent().makedirs() + FilePath(oldHostsOption).moveTo(hostsNonOption) + self.replaceHome(tmpdir) + self.options['known-hosts'] = None + default.verifyHostKey(self.fakeTransport, "4.3.2.1", sampleKey, + "I don't care.").addCallback(l.append) + self.assertEqual([1], l) + + + def test_verifyHostButNotIP(self): + """ + L{default.verifyHostKey} should return a L{Deferred} which fires with + C{1} when passed a host which matches with an IP is not present in its + known_hosts file, and should also warn the user that it has added the + IP address. + """ + l = [] + default.verifyHostKey(self.fakeTransport, "8.7.6.5", sampleKey, + "Fingerprint not required.").addCallback(l.append) + self.assertEqual( + ["Warning: Permanently added the RSA host key for IP address " + "'8.7.6.5' to the list of known hosts."], + self.fakeFile.outchunks) + self.assertEqual([1], l) + knownHostsFile = KnownHostsFile.fromPath(FilePath(self.hostsOption)) + self.assertEqual(True, knownHostsFile.hasHostKey("8.7.6.5", + Key.fromString(sampleKey))) + + + def test_verifyQuestion(self): + """ + L{default.verifyHostKey} should return a L{Default} which fires with + C{0} when passed a unknown host that the user refuses to acknowledge. + """ + self.fakeTransport.factory.options['host'] = 'fake.example.com' + l = [] + self.fakeFile.inlines.append("no") + default.verifyHostKey(self.fakeTransport, "9.8.7.6", otherSampleKey, + "No fingerprint!").addErrback(l.append) + self.assertEqual([ + "The authenticity of host 'fake.example.com (9.8.7.6)' " + "can't be established.\n" + "RSA key fingerprint is " + "57:a1:c2:a1:07:a0:2b:f4:ce:b5:e5:b7:ae:cc:e1:99.\n" + "Are you sure you want to continue connecting (yes/no)? "], + self.fakeFile.outchunks) + l[0].trap(UserRejectedKey) + + + def test_verifyBadKey(self): + """ + L{default.verifyHostKey} should return a L{Deferred} which fails with + L{HostKeyChanged} if the host key is incorrect. + """ + l = [] + default.verifyHostKey(self.fakeTransport, "4.3.2.1", otherSampleKey, + "Again, not required.").addErrback(l.append) + l[0].trap(HostKeyChanged) + diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_manhole.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_manhole.py --- twisted-conch-0.8.0/twisted/conch/test/test_manhole.py 2006-07-04 12:27:41.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_manhole.py 2007-05-20 23:42:53.000000000 +0100 @@ -1,15 +1,20 @@ # -*- test-case-name: twisted.conch.test.test_manhole -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2007 Twisted Matrix Laboratories. # See LICENSE for details. -from __future__ import generators +""" +Tests for L{twisted.conch.manhole}. +""" import traceback from twisted.trial import unittest from twisted.internet import error, defer +from twisted.test.proto_helpers import StringTransport from twisted.conch.test.test_recvline import _TelnetMixin, _SSHMixin, _StdioMixin, stdio, ssh from twisted.conch import manhole +from twisted.conch.insults import insults + def determineDefaultFunctionName(): """ @@ -23,6 +28,42 @@ defaultFunctionName = determineDefaultFunctionName() + +class ManholeInterpreterTests(unittest.TestCase): + """ + Tests for L{manhole.ManholeInterpreter}. + """ + def test_resetBuffer(self): + """ + L{ManholeInterpreter.resetBuffer} should empty the input buffer. + """ + interpreter = manhole.ManholeInterpreter(None) + interpreter.buffer.extend(["1", "2"]) + interpreter.resetBuffer() + self.assertFalse(interpreter.buffer) + + + +class ManholeProtocolTests(unittest.TestCase): + """ + Tests for L{manhole.Manhole}. + """ + def test_interruptResetsInterpreterBuffer(self): + """ + L{manhole.Manhole.handle_INT} should cause the interpreter input buffer + to be reset. + """ + transport = StringTransport() + terminal = insults.ServerProtocol(manhole.Manhole) + terminal.makeConnection(transport) + protocol = terminal.terminalProtocol + interpreter = protocol.interpreter + interpreter.buffer.extend(["1", "2"]) + protocol.handle_INT() + self.assertFalse(interpreter.buffer) + + + class WriterTestCase(unittest.TestCase): def testInteger(self): manhole.lastColorizedLine("1") @@ -163,6 +204,36 @@ return done.addCallback(finished) + + def test_interruptDuringContinuation(self): + """ + Sending ^C to Manhole while in a state where more input is required to + complete a statement should discard the entire ongoing statement and + reset the input prompt to the non-continuation prompt. + """ + continuing = self.recvlineClient.expect("things") + + self._testwrite("(\nthings") + + def gotContinuation(ignored): + self._assertBuffer( + [">>> (", + "... things"]) + interrupted = self.recvlineClient.expect(">>> ") + self._testwrite(manhole.CTRL_C) + return interrupted + continuing.addCallback(gotContinuation) + + def gotInterruption(ignored): + self._assertBuffer( + [">>> (", + "... things", + "KeyboardInterrupt", + ">>> "]) + continuing.addCallback(gotInterruption) + return continuing + + def testControlBackslash(self): self._testwrite("cancelled line") partialLine = self.recvlineClient.expect("cancelled line") diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_openssh_compat.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_openssh_compat.py --- twisted-conch-0.8.0/twisted/conch/test/test_openssh_compat.py 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_openssh_compat.py 2008-09-10 15:58:45.000000000 +0100 @@ -0,0 +1,101 @@ +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Tests for L{twisted.conch.openssh_compat}. +""" + +import os + +from twisted.trial.unittest import TestCase +from twisted.python.filepath import FilePath +from twisted.python.compat import set + +try: + import Crypto.Cipher.DES3 +except ImportError: + OpenSSHFactory = None +else: + from twisted.conch.openssh_compat.factory import OpenSSHFactory + +from twisted.conch.test import keydata +from twisted.test.test_process import MockOS + + +class OpenSSHFactoryTests(TestCase): + """ + Tests for L{OpenSSHFactory}. + """ + if getattr(os, "geteuid", None) is None: + skip = "geteuid/seteuid not available" + elif OpenSSHFactory is None: + skip = "Cannot run without PyCrypto" + + def setUp(self): + self.factory = OpenSSHFactory() + self.keysDir = FilePath(self.mktemp()) + self.keysDir.makedirs() + self.factory.dataRoot = self.keysDir.path + + self.keysDir.child("ssh_host_foo").setContent("foo") + self.keysDir.child("bar_key").setContent("foo") + self.keysDir.child("ssh_host_one_key").setContent( + keydata.privateRSA_openssh) + self.keysDir.child("ssh_host_two_key").setContent( + keydata.privateDSA_openssh) + self.keysDir.child("ssh_host_three_key").setContent( + "not a key content") + + self.keysDir.child("ssh_host_one_key.pub").setContent( + keydata.publicRSA_openssh) + + self.mockos = MockOS() + self.patch(os, "seteuid", self.mockos.seteuid) + self.patch(os, "setegid", self.mockos.setegid) + + + def test_getPublicKeys(self): + """ + L{OpenSSHFactory.getPublicKeys} should return the available public keys + in the data directory + """ + keys = self.factory.getPublicKeys() + self.assertEquals(len(keys), 1) + keyTypes = keys.keys() + self.assertEqual(keyTypes, ['ssh-rsa']) + + + def test_getPrivateKeys(self): + """ + L{OpenSSHFactory.getPrivateKeys} should return the available private + keys in the data directory. + """ + keys = self.factory.getPrivateKeys() + self.assertEquals(len(keys), 2) + keyTypes = keys.keys() + self.assertEqual(set(keyTypes), set(['ssh-rsa', 'ssh-dss'])) + self.assertEquals(self.mockos.seteuidCalls, []) + self.assertEquals(self.mockos.setegidCalls, []) + + + def test_getPrivateKeysAsRoot(self): + """ + L{OpenSSHFactory.getPrivateKeys} should switch to root if the keys + aren't readable by the current user. + """ + keyFile = self.keysDir.child("ssh_host_two_key") + # Fake permission error by changing the mode + keyFile.chmod(0000) + self.addCleanup(keyFile.chmod, 0777) + # And restore the right mode when seteuid is called + savedSeteuid = os.seteuid + def seteuid(euid): + keyFile.chmod(0777) + return savedSeteuid(euid) + self.patch(os, "seteuid", seteuid) + keys = self.factory.getPrivateKeys() + self.assertEquals(len(keys), 2) + keyTypes = keys.keys() + self.assertEqual(set(keyTypes), set(['ssh-rsa', 'ssh-dss'])) + self.assertEquals(self.mockos.seteuidCalls, [0, os.geteuid()]) + self.assertEquals(self.mockos.setegidCalls, [0, os.getegid()]) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_recvline.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_recvline.py --- twisted-conch-0.8.0/twisted/conch/test/test_recvline.py 2006-10-03 01:01:42.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_recvline.py 2008-01-06 15:07:47.000000000 +0000 @@ -1,7 +1,12 @@ # -*- test-case-name: twisted.conch.test.test_recvline -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE for details. +""" +Tests for L{twisted.conch.recvline} and fixtures for testing related +functionality. +""" + import sys, os from twisted.conch.insults import insults @@ -474,9 +479,7 @@ except ImportError: stdio = None -from twisted.test.test_process import SignalMixin - -class _StdioMixin(_BaseMixin, SignalMixin): +class _StdioMixin(_BaseMixin): def setUp(self): # A memory-only terminal emulator, into which the server will # write things and make other state changes. What ends up @@ -499,7 +502,9 @@ # handle bytes we send to the child process. exe = sys.executable module = stdio.__file__ - args = ["python2.3", module, reflect.qual(self.serverProtocol)] + if module.endswith('.pyc') or module.endswith('.pyo'): + module = module[:-1] + args = [exe, module, reflect.qual(self.serverProtocol)] env = {"PYTHONPATH": os.pathsep.join(sys.path)} from twisted.internet import reactor @@ -521,7 +526,7 @@ # Kill the child process. We're done with it. try: self.clientTransport.signalProcess("KILL") - except OSError: + except (error.ProcessExitedAlready, OSError): pass def trap(failure): failure.trap(error.ProcessTerminated) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_ssh.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_ssh.py --- twisted-conch-0.8.0/twisted/conch/test/test_ssh.py 2006-10-08 23:23:48.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_ssh.py 2008-09-20 04:15:34.000000000 +0100 @@ -1,14 +1,18 @@ # -*- test-case-name: twisted.conch.test.test_ssh -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2007 Twisted Matrix Laboratories. # See LICENSE for details. +import struct + try: - import Crypto + import Crypto.Cipher.DES3 except ImportError: Crypto = None from twisted.conch.ssh import common, session, forwarding from twisted.conch import avatar, error +from twisted.conch.test.keydata import publicRSA_openssh, privateRSA_openssh +from twisted.conch.test.keydata import publicDSA_openssh, privateDSA_openssh from twisted.cred import portal from twisted.internet import defer, protocol, reactor from twisted.internet.error import ProcessTerminated @@ -17,7 +21,6 @@ from test_recvline import LoopbackRelay -import struct class ConchTestRealm: @@ -225,25 +228,12 @@ self.proto.errConnectionLost() self.proto.processEnded(failure.Failure(ProcessTerminated(0, None, None))) -class _LogTimeFormatMixin: - - def setUpClass(self): - from twisted.python import log - self._oldTimeFormat = log.FileLogObserver.timeFormat - log.FileLogObserver.timeFormat = '%Y/%m/%d %H:%M:%S %Z' - - def tearDownClass(self): - log.FileLogObserver.timeFormat = self._oldTimeFormat if Crypto: # stuff that needs PyCrypto to even import from twisted.conch import checkers from twisted.conch.ssh import channel, connection, factory, keys from twisted.conch.ssh import transport, userauth - from test_keys import publicRSA_openssh, privateRSA_openssh - from test_keys import publicDSA_openssh, privateDSA_openssh - - class UtilityTestCase(unittest.TestCase): def testCounter(self): c = transport._Counter('\x00\x00', 2) @@ -734,19 +724,149 @@ if not Crypto: skip = "can't run w/o PyCrypto" - def testMultipleFactories(self): - f1 = factory.SSHFactory() - f2 = factory.SSHFactory() - gpk = lambda: {'ssh-rsa' : "don't use"} - f1.getPrimes = lambda: None - f2.getPrimes = lambda: {1:(2,3)} - f1.getPublicKeys = f2.getPublicKeys = gpk - f1.getPrivateKeys = f2.getPrivateKeys = gpk - f1.startFactory() - f2.startFactory() + def makeSSHFactory(self, primes=None): + sshFactory = factory.SSHFactory() + gpk = lambda: {'ssh-rsa' : keys.Key(None)} + sshFactory.getPrimes = lambda: primes + sshFactory.getPublicKeys = sshFactory.getPrivateKeys = gpk + sshFactory.startFactory() + return sshFactory + + + def test_buildProtocol(self): + """ + By default, buildProtocol() constructs an instance of + SSHServerTransport. + """ + factory = self.makeSSHFactory() + protocol = factory.buildProtocol(None) + self.assertIsInstance(protocol, transport.SSHServerTransport) + + + def test_buildProtocolRespectsProtocol(self): + """ + buildProtocol() calls 'self.protocol()' to construct a protocol + instance. + """ + calls = [] + def makeProtocol(*args): + calls.append(args) + return transport.SSHServerTransport() + factory = self.makeSSHFactory() + factory.protocol = makeProtocol + protocol = factory.buildProtocol(None) + self.assertEquals([()], calls) + + + def test_multipleFactories(self): + f1 = self.makeSSHFactory(primes=None) + f2 = self.makeSSHFactory(primes={1:(2,3)}) p1 = f1.buildProtocol(None) p2 = f2.buildProtocol(None) self.failIf('diffie-hellman-group-exchange-sha1' in p1.supportedKeyExchanges, p1.supportedKeyExchanges) self.failUnless('diffie-hellman-group-exchange-sha1' in p2.supportedKeyExchanges, p2.supportedKeyExchanges) + + +class EntropyTestCase(unittest.TestCase): + """ + Tests for L{common.entropy}. + """ + + def test_deprecation(self): + """ + Test the deprecation of L{common.entropy.get_bytes}. + """ + def wrapper(): + return common.entropy.get_bytes(10) + self.assertWarns(DeprecationWarning, + "entropy.get_bytes is deprecated, please use " + "twisted.python.randbytes.secureRandom instead.", + __file__, wrapper) + + + +class MPTestCase(unittest.TestCase): + """ + Tests for L{common.getMP}. + + @cvar getMP: a method providing a MP parser. + @type getMP: C{callable} + """ + getMP = staticmethod(common.getMP) + + if not Crypto: + skip = "can't run w/o PyCrypto" + + + def test_getMP(self): + """ + L{common.getMP} should parse the a multiple precision integer from a + string: a 4-byte length followed by length bytes of the integer. + """ + self.assertEquals( + self.getMP('\x00\x00\x00\x04\x00\x00\x00\x01'), + (1, '')) + + + def test_getMPBigInteger(self): + """ + L{common.getMP} should be able to parse a big enough integer + (that doesn't fit on one byte). + """ + self.assertEquals( + self.getMP('\x00\x00\x00\x04\x01\x02\x03\x04'), + (16909060, '')) + + + def test_multipleGetMP(self): + """ + L{common.getMP} has the ability to parse multiple integer in the same + string. + """ + self.assertEquals( + self.getMP('\x00\x00\x00\x04\x00\x00\x00\x01' + '\x00\x00\x00\x04\x00\x00\x00\x02', 2), + (1, 2, '')) + + + def test_getMPRemainingData(self): + """ + When more data than needed is sent to L{common.getMP}, it should return + the remaining data. + """ + self.assertEquals( + self.getMP('\x00\x00\x00\x04\x00\x00\x00\x01foo'), + (1, 'foo')) + + + def test_notEnoughData(self): + """ + When the string passed to L{common.getMP} doesn't even make 5 bytes, + it should raise a L{struct.error}. + """ + self.assertRaises(struct.error, self.getMP, '\x02\x00') + + + +class PyMPTestCase(MPTestCase): + """ + Tests for the python implementation of L{common.getMP}. + """ + getMP = staticmethod(common.getMP_py) + + + +class GMPYMPTestCase(MPTestCase): + """ + Tests for the gmpy implementation of L{common.getMP}. + """ + getMP = staticmethod(common._fastgetMP) + + + +try: + import gmpy +except ImportError: + GMPYMPTestCase.skip = "gmpy not available" diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_telnet.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_telnet.py --- twisted-conch-0.8.0/twisted/conch/test/test_telnet.py 2006-12-29 21:24:23.000000000 +0000 +++ twisted-conch-8.2.0/twisted/conch/test/test_telnet.py 2008-07-09 15:53:53.000000000 +0100 @@ -1,7 +1,11 @@ # -*- test-case-name: twisted.conch.test.test_telnet -*- -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2007 Twisted Matrix Laboratories. # See LICENSE for details. +""" +Tests for L{twisted.conch.telnet}. +""" + from zope.interface import implements from twisted.internet import defer @@ -62,7 +66,12 @@ def disableRemote(self, option): self.disabledRemote.append(option) -class TelnetTestCase(unittest.TestCase): + + +class TelnetTransportTestCase(unittest.TestCase): + """ + Tests for L{telnet.TelnetTransport}. + """ def setUp(self): self.p = telnet.TelnetTransport(TestProtocol) self.t = proto_helpers.StringTransport() @@ -485,3 +494,185 @@ d2 = self.assertFailure(d2, TestException) d3 = self.assertFailure(d3, TestException) return defer.gatherResults([d1, d2, d3]) + + +class TestTelnet(telnet.Telnet): + """ + A trivial extension of the telnet protocol class useful to unit tests. + """ + def __init__(self): + telnet.Telnet.__init__(self) + self.events = [] + + + def applicationDataReceived(self, bytes): + """ + Record the given data in C{self.events}. + """ + self.events.append(('bytes', bytes)) + + + def unhandledCommand(self, command, bytes): + """ + Record the given command in C{self.events}. + """ + self.events.append(('command', command, bytes)) + + + def unhandledSubnegotiation(self, command, bytes): + """ + Record the given subnegotiation command in C{self.events}. + """ + self.events.append(('negotiate', command, bytes)) + + + +class TelnetTests(unittest.TestCase): + """ + Tests for L{telnet.Telnet}. + + L{telnet.Telnet} implements the TELNET protocol (RFC 854), including option + and suboption negotiation, and option state tracking. + """ + def setUp(self): + """ + Create an unconnected L{telnet.Telnet} to be used by tests. + """ + self.protocol = TestTelnet() + + + def test_enableLocal(self): + """ + L{telnet.Telnet.enableLocal} should reject all options, since + L{telnet.Telnet} does not know how to implement any options. + """ + self.assertFalse(self.protocol.enableLocal('\0')) + + + def test_enableRemote(self): + """ + L{telnet.Telnet.enableRemote} should reject all options, since + L{telnet.Telnet} does not know how to implement any options. + """ + self.assertFalse(self.protocol.enableRemote('\0')) + + + def test_disableLocal(self): + """ + It is an error for L{telnet.Telnet.disableLocal} to be called, since + L{telnet.Telnet.enableLocal} will never allow any options to be enabled + locally. If a subclass overrides enableLocal, it must also override + disableLocal. + """ + self.assertRaises(NotImplementedError, self.protocol.disableLocal, '\0') + + + def test_disableRemote(self): + """ + It is an error for L{telnet.Telnet.disableRemote} to be called, since + L{telnet.Telnet.enableRemote} will never allow any options to be + enabled remotely. If a subclass overrides enableRemote, it must also + override disableRemote. + """ + self.assertRaises(NotImplementedError, self.protocol.disableRemote, '\0') + + + def _deliver(self, bytes, *expected): + """ + Pass the given bytes to the protocol's C{dataReceived} method and + assert that the given events occur. + """ + received = self.protocol.events = [] + self.protocol.dataReceived(bytes) + self.assertEqual(received, list(expected)) + + + def test_oneApplicationDataByte(self): + """ + One application-data byte in the default state gets delivered right + away. + """ + self._deliver('a', ('bytes', 'a')) + + + def test_twoApplicationDataBytes(self): + """ + Two application-data bytes in the default state get delivered + together. + """ + self._deliver('bc', ('bytes', 'bc')) + + + def test_threeApplicationDataBytes(self): + """ + Three application-data bytes followed by a control byte get + delivered, but the control byte doesn't. + """ + self._deliver('def' + telnet.IAC, ('bytes', 'def')) + + + def test_escapedControl(self): + """ + IAC in the escaped state gets delivered and so does another + application-data byte following it. + """ + self._deliver(telnet.IAC) + self._deliver(telnet.IAC + 'g', ('bytes', telnet.IAC + 'g')) + + + def test_carriageReturn(self): + """ + A carriage return only puts the protocol into the newline state. A + linefeed in the newline state causes just the newline to be + delivered. A nul in the newline state causes a carriage return to + be delivered. An IAC in the newline state causes a carriage return + to be delivered and puts the protocol into the escaped state. + Anything else causes a carriage return and that thing to be + delivered. + """ + self._deliver('\r') + self._deliver('\n', ('bytes', '\n')) + self._deliver('\r\n', ('bytes', '\n')) + + self._deliver('\r') + self._deliver('\0', ('bytes', '\r')) + self._deliver('\r\0', ('bytes', '\r')) + + self._deliver('\r') + self._deliver('a', ('bytes', '\ra')) + self._deliver('\ra', ('bytes', '\ra')) + + self._deliver('\r') + self._deliver( + telnet.IAC + telnet.IAC + 'x', ('bytes', '\r' + telnet.IAC + 'x')) + + + def test_applicationDataBeforeSimpleCommand(self): + """ + Application bytes received before a command are delivered before the + command is processed. + """ + self._deliver( + 'x' + telnet.IAC + telnet.NOP, + ('bytes', 'x'), ('command', telnet.NOP, None)) + + + def test_applicationDataBeforeCommand(self): + """ + Application bytes received before a WILL/WONT/DO/DONT are delivered + before the command is processed. + """ + self.protocol.commandMap = {} + self._deliver( + 'y' + telnet.IAC + telnet.WILL + '\x00', + ('bytes', 'y'), ('command', telnet.WILL, '\x00')) + + + def test_applicationDataBeforeSubnegotiation(self): + """ + Application bytes received before a subnegotiation command are + delivered before the negotiation is processed. + """ + self._deliver( + 'z' + telnet.IAC + telnet.SB + 'Qx' + telnet.IAC + telnet.SE, + ('bytes', 'z'), ('negotiate', 'Q', ['x'])) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_transport.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_transport.py --- twisted-conch-0.8.0/twisted/conch/test/test_transport.py 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_transport.py 2008-09-10 15:58:45.000000000 +0100 @@ -0,0 +1,1933 @@ +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Tests for ssh/transport.py and the classes therein. +""" + +import md5, sha + +try: + import Crypto.Cipher.DES3 +except ImportError: + Crypto = None + class transport: # fictional modules to make classes work + class SSHTransportBase: pass + class SSHServerTransport: pass + class SSHClientTransport: pass + class factory: + class SSHFactory: + pass +else: + from twisted.conch.ssh import transport, common, keys, factory + from twisted.conch.test import keydata + +from twisted.trial import unittest +from twisted.internet import defer +from twisted.protocols import loopback +from twisted.python import randbytes +from twisted.python.reflect import qual +from twisted.conch.ssh import service +from twisted.test import proto_helpers + +from twisted.conch.error import ConchError + + + +class MockTransportBase(transport.SSHTransportBase): + """ + A base class for the client and server protocols. Stores the messages + it receieves instead of ignoring them. + + @ivar errors: a list of tuples: (reasonCode, description) + @ivar unimplementeds: a list of integers: sequence number + @ivar debugs: a list of tuples: (alwaysDisplay, message, lang) + @ivar ignoreds: a list of strings: ignored data + """ + + + def connectionMade(self): + """ + Set up instance variables. + """ + transport.SSHTransportBase.connectionMade(self) + self.errors = [] + self.unimplementeds = [] + self.debugs = [] + self.ignoreds = [] + + + def receiveError(self, reasonCode, description): + """ + Store any errors received. + + @type reasonCode: C{int} + @type description: C{str} + """ + self.errors.append((reasonCode, description)) + + + def receiveUnimplemented(self, seqnum): + """ + Store any unimplemented packet messages. + + @type seqnum: C{int} + """ + self.unimplementeds.append(seqnum) + + + def receiveDebug(self, alwaysDisplay, message, lang): + """ + Store any debug messages. + + @type alwaysDisplay: C{bool} + @type message: C{str} + @type lang: C{str} + """ + self.debugs.append((alwaysDisplay, message, lang)) + + + def ssh_IGNORE(self, packet): + """ + Store any ignored data. + + @type packet: C{str} + """ + self.ignoreds.append(packet) + + +class MockCipher(object): + """ + A mocked-up version of twisted.conch.ssh.transport.SSHCiphers. + """ + outCipType = 'test' + encBlockSize = 6 + inCipType = 'test' + decBlockSize = 6 + inMACType = 'test' + outMACType = 'test' + verifyDigestSize = 1 + usedEncrypt = False + usedDecrypt = False + outMAC = (None, '', '', 1) + inMAC = (None, '', '', 1) + keys = () + + + def encrypt(self, x): + """ + Called to encrypt the packet. Simply record that encryption was used + and return the data unchanged. + """ + self.usedEncrypt = True + if (len(x) % self.encBlockSize) != 0: + raise RuntimeError("length %i modulo blocksize %i is not 0: %i" % + (len(x), self.encBlockSize, len(x) % self.encBlockSize)) + return x + + + def decrypt(self, x): + """ + Called to decrypt the packet. Simply record that decryption was used + and return the data unchanged. + """ + self.usedDecrypt = True + if (len(x) % self.encBlockSize) != 0: + raise RuntimeError("length %i modulo blocksize %i is not 0: %i" % + (len(x), self.decBlockSize, len(x) % self.decBlockSize)) + return x + + + def makeMAC(self, outgoingPacketSequence, payload): + """ + Make a Message Authentication Code by sending the character value of + the outgoing packet. + """ + return chr(outgoingPacketSequence) + + + def verify(self, incomingPacketSequence, packet, macData): + """ + Verify the Message Authentication Code by checking that the packet + sequence number is the same. + """ + return chr(incomingPacketSequence) == macData + + + def setKeys(self, ivOut, keyOut, ivIn, keyIn, macIn, macOut): + """ + Record the keys. + """ + self.keys = (ivOut, keyOut, ivIn, keyIn, macIn, macOut) + + + +class MockCompression: + """ + A mocked-up compression, based on the zlib interface. Instead of + compressing, it reverses the data and adds a 0x66 byte to the end. + """ + + + def compress(self, payload): + return payload[::-1] # reversed + + + def decompress(self, payload): + return payload[:-1][::-1] + + + def flush(self, kind): + return '\x66' + + + +class MockService(service.SSHService): + """ + A mocked-up service, based on twisted.conch.ssh.service.SSHService. + + @ivar started: True if this service has been started. + @ivar stopped: True if this service has been stopped. + """ + name = "MockService" + started = False + stopped = False + protocolMessages = {0xff: "MSG_TEST", 71: "MSG_fiction"} + + + def logPrefix(self): + return "MockService" + + + def serviceStarted(self): + """ + Record that the service was started. + """ + self.started = True + + + def serviceStopped(self): + """ + Record that the service was stopped. + """ + self.stopped = True + + + def ssh_TEST(self, packet): + """ + A message that this service responds to. + """ + self.transport.sendPacket(0xff, packet) + + +class MockFactory(factory.SSHFactory): + """ + A mocked-up factory based on twisted.conch.ssh.factory.SSHFactory. + """ + services = { + 'ssh-userauth': MockService} + + + def getPublicKeys(self): + """ + Return the public keys that authenticate this server. + """ + return { + 'ssh-rsa': keys.Key.fromString(keydata.publicRSA_openssh), + 'ssh-dsa': keys.Key.fromString(keydata.publicDSA_openssh)} + + + def getPrivateKeys(self): + """ + Return the private keys that authenticate this server. + """ + return { + 'ssh-rsa': keys.Key.fromString(keydata.privateRSA_openssh), + 'ssh-dsa': keys.Key.fromString(keydata.privateDSA_openssh)} + + + def getPrimes(self): + """ + Return the Diffie-Hellman primes that can be used for the + diffie-hellman-group-exchange-sha1 key exchange. + """ + return { + 1024: ((2, transport.DH_PRIME),), + 2048: ((3, transport.DH_PRIME),), + 4096: ((5, 7),)} + + + +class MockOldFactoryPublicKeys(MockFactory): + """ + The old SSHFactory returned mappings from key names to strings from + getPublicKeys(). We return those here for testing. + """ + + + def getPublicKeys(self): + """ + We used to map key types to public key blobs as strings. + """ + keys = MockFactory.getPublicKeys(self) + for name, key in keys.items()[:]: + keys[name] = key.blob() + return keys + + + +class MockOldFactoryPrivateKeys(MockFactory): + """ + The old SSHFactory returned mappings from key names to PyCrypto key + objects from getPrivateKeys(). We return those here for testing. + """ + + + def getPrivateKeys(self): + """ + We used to map key types to PyCrypto key objects. + """ + keys = MockFactory.getPrivateKeys(self) + for name, key in keys.items()[:]: + keys[name] = key.keyObject + return keys + + + +class TransportTestCase(unittest.TestCase): + """ + Base class for transport test cases. + """ + klass = None + + if Crypto is None: + skip = "cannot run w/o PyCrypto" + + + def setUp(self): + self.transport = proto_helpers.StringTransport() + self.proto = self.klass() + self.packets = [] + def secureRandom(len): + """ + Return a consistent entropy value + """ + return '\x99' * len + self.oldSecureRandom = randbytes.secureRandom + randbytes.secureRandom = secureRandom + def stubSendPacket(messageType, payload): + self.packets.append((messageType, payload)) + self.proto.makeConnection(self.transport) + # we just let the kex packet go into the transport + self.proto.sendPacket = stubSendPacket + + + def tearDown(self): + randbytes.secureRandom = self.oldSecureRandom + self.oldSecureRandom = None + + + +class BaseSSHTransportTestCase(TransportTestCase): + """ + Test TransportBase. It implements the non-server/client specific + parts of the SSH transport protocol. + """ + + klass = MockTransportBase + + + def test_sendVersion(self): + """ + Test that the first thing sent over the connection is the version + string. + """ + # the other setup was done in the setup method + self.assertEquals(self.transport.value().split('\r\n', 1)[0], + "SSH-2.0-Twisted") + + + def test_sendPacketPlain(self): + """ + Test that plain (unencrypted, uncompressed) packets are sent + correctly. The format is:: + uint32 length (including type and padding length) + byte padding length + byte type + bytes[length-padding length-2] data + bytes[padding length] padding + """ + proto = MockTransportBase() + proto.makeConnection(self.transport) + self.transport.clear() + message = ord('A') + payload = 'BCDEFG' + proto.sendPacket(message, payload) + value = self.transport.value() + self.assertEquals(value, '\x00\x00\x00\x0c\x04ABCDEFG\x99\x99\x99\x99') + + + def test_sendPacketEncrypted(self): + """ + Test that packets sent while encryption is enabled are sent + correctly. The whole packet should be encrypted. + """ + proto = MockTransportBase() + proto.makeConnection(self.transport) + proto.currentEncryptions = testCipher = MockCipher() + message = ord('A') + payload = 'BC' + self.transport.clear() + proto.sendPacket(message, payload) + self.assertTrue(testCipher.usedEncrypt) + value = self.transport.value() + self.assertEquals(value, '\x00\x00\x00\x08\x04ABC\x99\x99\x99\x99\x01') + + + def test_sendPacketCompressed(self): + """ + Test that packets sent while compression is enabled are sent + correctly. The packet type and data should be encrypted. + """ + proto = MockTransportBase() + proto.makeConnection(self.transport) + proto.outgoingCompression = MockCompression() + self.transport.clear() + proto.sendPacket(ord('A'), 'B') + value = self.transport.value() + self.assertEquals( + value, + '\x00\x00\x00\x0c\x08BA\x66\x99\x99\x99\x99\x99\x99\x99\x99') + + + def test_sendPacketBoth(self): + """ + Test that packets sent while compression and encryption are + enabled are sent correctly. The packet type and data should be + compressed and then the whole packet should be encrypted. + """ + proto = MockTransportBase() + proto.makeConnection(self.transport) + proto.currentEncryptions = testCipher = MockCipher() + proto.outgoingCompression = MockCompression() + message = ord('A') + payload = 'BC' + self.transport.clear() + proto.sendPacket(message, payload) + value = self.transport.value() + self.assertEquals( + value, + '\x00\x00\x00\x0e\x09CBA\x66\x99\x99\x99\x99\x99\x99\x99\x99\x99' + '\x01') + + + def test_getPacketPlain(self): + """ + Test that packets are retrieved correctly out of the buffer when + no encryption is enabled. + """ + proto = MockTransportBase() + proto.makeConnection(self.transport) + self.transport.clear() + proto.sendPacket(ord('A'), 'BC') + proto.buf = self.transport.value() + 'extra' + self.assertEquals(proto.getPacket(), 'ABC') + self.assertEquals(proto.buf, 'extra') + + + def test_getPacketEncrypted(self): + """ + Test that encrypted packets are retrieved correctly. + See test_sendPacketEncrypted. + """ + proto = MockTransportBase() + proto.sendKexInit = lambda: None # don't send packets + proto.makeConnection(self.transport) + self.transport.clear() + proto.currentEncryptions = testCipher = MockCipher() + proto.sendPacket(ord('A'), 'BCD') + value = self.transport.value() + proto.buf = value[:MockCipher.decBlockSize] + self.assertEquals(proto.getPacket(), None) + self.assertTrue(testCipher.usedDecrypt) + self.assertEquals(proto.first, '\x00\x00\x00\x0e\x09A') + proto.buf += value[MockCipher.decBlockSize:] + self.assertEquals(proto.getPacket(), 'ABCD') + self.assertEquals(proto.buf, '') + + + def test_getPacketCompressed(self): + """ + Test that compressed packets are retrieved correctly. See + test_sendPacketCompressed. + """ + proto = MockTransportBase() + proto.makeConnection(self.transport) + self.transport.clear() + proto.outgoingCompression = MockCompression() + proto.incomingCompression = proto.outgoingCompression + proto.sendPacket(ord('A'), 'BCD') + proto.buf = self.transport.value() + self.assertEquals(proto.getPacket(), 'ABCD') + + + def test_getPacketBoth(self): + """ + Test that compressed and encrypted packets are retrieved correctly. + See test_sendPacketBoth. + """ + proto = MockTransportBase() + proto.sendKexInit = lambda: None + proto.makeConnection(self.transport) + self.transport.clear() + proto.currentEncryptions = testCipher = MockCipher() + proto.outgoingCompression = MockCompression() + proto.incomingCompression = proto.outgoingCompression + proto.sendPacket(ord('A'), 'BCDEFG') + proto.buf = self.transport.value() + self.assertEquals(proto.getPacket(), 'ABCDEFG') + + + def test_ciphersAreValid(self): + """ + Test that all the supportedCiphers are valid. + """ + ciphers = transport.SSHCiphers('A', 'B', 'C', 'D') + iv = key = '\x00' * 16 + for cipName in self.proto.supportedCiphers: + self.assertTrue(ciphers._getCipher(cipName, iv, key)) + + + def test_sendKexInit(self): + """ + Test that the KEXINIT (key exchange initiation) message is sent + correctly. Payload:: + bytes[16] cookie + string key exchange algorithms + string public key algorithms + string outgoing ciphers + string incoming ciphers + string outgoing MACs + string incoming MACs + string outgoing compressions + string incoming compressions + bool first packet follows + uint32 0 + """ + value = self.transport.value().split('\r\n', 1)[1] + self.proto.buf = value + packet = self.proto.getPacket() + self.assertEquals(packet[0], chr(transport.MSG_KEXINIT)) + self.assertEquals(packet[1:17], '\x99' * 16) + (kex, pubkeys, ciphers1, ciphers2, macs1, macs2, compressions1, + compressions2, languages1, languages2, + buf) = common.getNS(packet[17:], 10) + + self.assertEquals(kex, ','.join(self.proto.supportedKeyExchanges)) + self.assertEquals(pubkeys, ','.join(self.proto.supportedPublicKeys)) + self.assertEquals(ciphers1, ','.join(self.proto.supportedCiphers)) + self.assertEquals(ciphers2, ','.join(self.proto.supportedCiphers)) + self.assertEquals(macs1, ','.join(self.proto.supportedMACs)) + self.assertEquals(macs2, ','.join(self.proto.supportedMACs)) + self.assertEquals(compressions1, + ','.join(self.proto.supportedCompressions)) + self.assertEquals(compressions2, + ','.join(self.proto.supportedCompressions)) + self.assertEquals(languages1, ','.join(self.proto.supportedLanguages)) + self.assertEquals(languages2, ','.join(self.proto.supportedLanguages)) + self.assertEquals(buf, '\x00' * 5) + + + def test_sendDebug(self): + """ + Test that debug messages are sent correctly. Payload:: + bool always display + string debug message + string language + """ + self.proto.sendDebug("test", True, 'en') + self.assertEquals( + self.packets, + [(transport.MSG_DEBUG, + "\x01\x00\x00\x00\x04test\x00\x00\x00\x02en")]) + + + def test_receiveDebug(self): + """ + Test that debug messages are received correctly. See test_sendDebug. + """ + self.proto.dispatchMessage( + transport.MSG_DEBUG, + '\x01\x00\x00\x00\x04test\x00\x00\x00\x02en') + self.assertEquals(self.proto.debugs, [(True, 'test', 'en')]) + + + def test_sendIgnore(self): + """ + Test that ignored messages are sent correctly. Payload:: + string ignored data + """ + self.proto.sendIgnore("test") + self.assertEquals( + self.packets, [(transport.MSG_IGNORE, + '\x00\x00\x00\x04test')]) + + + def test_receiveIgnore(self): + """ + Test that ignored messages are received correctly. See + test_sendIgnore. + """ + self.proto.dispatchMessage(transport.MSG_IGNORE, 'test') + self.assertEquals(self.proto.ignoreds, ['test']) + + + def test_sendUnimplemented(self): + """ + Test that unimplemented messages are sent correctly. Payload:: + uint32 sequence number + """ + self.proto.sendUnimplemented() + self.assertEquals( + self.packets, [(transport.MSG_UNIMPLEMENTED, + '\x00\x00\x00\x00')]) + + + def test_receiveUnimplemented(self): + """ + Test that unimplemented messages are received correctly. See + test_sendUnimplemented. + """ + self.proto.dispatchMessage(transport.MSG_UNIMPLEMENTED, + '\x00\x00\x00\xff') + self.assertEquals(self.proto.unimplementeds, [255]) + + + def test_sendDisconnect(self): + """ + Test that disconnection messages are sent correctly. Payload:: + uint32 reason code + string reason description + string language + """ + disconnected = [False] + def stubLoseConnection(): + disconnected[0] = True + self.transport.loseConnection = stubLoseConnection + self.proto.sendDisconnect(0xff, "test") + self.assertEquals( + self.packets, + [(transport.MSG_DISCONNECT, + "\x00\x00\x00\xff\x00\x00\x00\x04test\x00\x00\x00\x00")]) + self.assertTrue(disconnected[0]) + + + def test_receiveDisconnect(self): + """ + Test that disconnection messages are received correctly. See + test_sendDisconnect. + """ + disconnected = [False] + def stubLoseConnection(): + disconnected[0] = True + self.transport.loseConnection = stubLoseConnection + self.proto.dispatchMessage(transport.MSG_DISCONNECT, + '\x00\x00\x00\xff\x00\x00\x00\x04test') + self.assertEquals(self.proto.errors, [(255, 'test')]) + self.assertTrue(disconnected[0]) + + + def test_dataReceived(self): + """ + Test that dataReceived parses packets and dispatches them to + ssh_* methods. + """ + kexInit = [False] + def stubKEXINIT(packet): + kexInit[0] = True + self.proto.ssh_KEXINIT = stubKEXINIT + self.proto.dataReceived(self.transport.value()) + self.assertTrue(self.proto.gotVersion) + self.assertEquals(self.proto.ourVersionString, + self.proto.otherVersionString) + self.assertTrue(kexInit[0]) + + + def test_service(self): + """ + Test that the transport can set the running service and dispatches + packets to the service's packetReceived method. + """ + service = MockService() + self.proto.setService(service) + self.assertEquals(self.proto.service, service) + self.assertTrue(service.started) + self.proto.dispatchMessage(0xff, "test") + self.assertEquals(self.packets, [(0xff, "test")]) + + service2 = MockService() + self.proto.setService(service2) + self.assertTrue(service2.started) + self.assertTrue(service.stopped) + + self.proto.connectionLost(None) + self.assertTrue(service2.stopped) + + + def test_avatar(self): + """ + Test that the transport notifies the avatar of disconnections. + """ + disconnected = [False] + def logout(): + disconnected[0] = True + self.proto.logoutFunction = logout + self.proto.avatar = True + + self.proto.connectionLost(None) + self.assertTrue(disconnected[0]) + + + def test_isEncrypted(self): + """ + Test that the transport accurately reflects its encrypted status. + """ + self.assertFalse(self.proto.isEncrypted('in')) + self.assertFalse(self.proto.isEncrypted('out')) + self.assertFalse(self.proto.isEncrypted('both')) + self.proto.currentEncryptions = MockCipher() + self.assertTrue(self.proto.isEncrypted('in')) + self.assertTrue(self.proto.isEncrypted('out')) + self.assertTrue(self.proto.isEncrypted('both')) + self.proto.currentEncryptions = transport.SSHCiphers('none', 'none', + 'none', 'none') + self.assertFalse(self.proto.isEncrypted('in')) + self.assertFalse(self.proto.isEncrypted('out')) + self.assertFalse(self.proto.isEncrypted('both')) + + self.assertRaises(TypeError, self.proto.isEncrypted, 'bad') + + + def test_isVerified(self): + """ + Test that the transport accurately reflects its verified status. + """ + self.assertFalse(self.proto.isVerified('in')) + self.assertFalse(self.proto.isVerified('out')) + self.assertFalse(self.proto.isVerified('both')) + self.proto.currentEncryptions = MockCipher() + self.assertTrue(self.proto.isVerified('in')) + self.assertTrue(self.proto.isVerified('out')) + self.assertTrue(self.proto.isVerified('both')) + self.proto.currentEncryptions = transport.SSHCiphers('none', 'none', + 'none', 'none') + self.assertFalse(self.proto.isVerified('in')) + self.assertFalse(self.proto.isVerified('out')) + self.assertFalse(self.proto.isVerified('both')) + + self.assertRaises(TypeError, self.proto.isVerified, 'bad') + + + def test_loseConnection(self): + """ + Test that loseConnection sends a disconnect message and closes the + connection. + """ + disconnected = [False] + def stubLoseConnection(): + disconnected[0] = True + self.transport.loseConnection = stubLoseConnection + self.proto.loseConnection() + self.assertEquals(self.packets[0][0], transport.MSG_DISCONNECT) + self.assertEquals(self.packets[0][1][3], + chr(transport.DISCONNECT_CONNECTION_LOST)) + + + def test_badVersion(self): + """ + Test that the transport disconnects when it receives a bad version. + """ + def testBad(version): + self.packets = [] + self.proto.gotVersion = False + disconnected = [False] + def stubLoseConnection(): + disconnected[0] = True + self.transport.loseConnection = stubLoseConnection + for c in version + '\r\n': + self.proto.dataReceived(c) + self.assertTrue(disconnected[0]) + self.assertEquals(self.packets[0][0], transport.MSG_DISCONNECT) + self.assertEquals( + self.packets[0][1][3], + chr(transport.DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED)) + testBad('SSH-1.5-OpenSSH') + testBad('SSH-3.0-Twisted') + testBad('GET / HTTP/1.1') + + + def test_dataBeforeVersion(self): + """ + Test that the transport ignores data sent before the version string. + """ + proto = MockTransportBase() + proto.makeConnection(proto_helpers.StringTransport()) + data = ("""here's some stuff beforehand +here's some other stuff +""" + proto.ourVersionString + "\r\n") + [proto.dataReceived(c) for c in data] + self.assertTrue(proto.gotVersion) + self.assertEquals(proto.otherVersionString, proto.ourVersionString) + + + def test_compatabilityVersion(self): + """ + Test that the transport treats the compatbility version (1.99) + as equivalent to version 2.0. + """ + proto = MockTransportBase() + proto.makeConnection(proto_helpers.StringTransport()) + proto.dataReceived("SSH-1.99-OpenSSH\n") + self.assertTrue(proto.gotVersion) + self.assertEquals(proto.otherVersionString, "SSH-1.99-OpenSSH") + + + def test_badPackets(self): + """ + Test that the transport disconnects with an error when it receives + bad packets. + """ + def testBad(packet, error=transport.DISCONNECT_PROTOCOL_ERROR): + self.packets = [] + self.proto.buf = packet + self.assertEquals(self.proto.getPacket(), None) + self.assertEquals(len(self.packets), 1) + self.assertEquals(self.packets[0][0], transport.MSG_DISCONNECT) + self.assertEquals(self.packets[0][1][3], chr(error)) + + testBad('\xff' * 8) # big packet + testBad('\x00\x00\x00\x05\x00BCDE') # length not modulo blocksize + oldEncryptions = self.proto.currentEncryptions + self.proto.currentEncryptions = MockCipher() + testBad('\x00\x00\x00\x08\x06AB123456', # bad MAC + transport.DISCONNECT_MAC_ERROR) + self.proto.currentEncryptions.decrypt = lambda x: x[:-1] + testBad('\x00\x00\x00\x08\x06BCDEFGHIJK') # bad decryption + self.proto.currentEncryptions = oldEncryptions + self.proto.incomingCompression = MockCompression() + def stubDecompress(payload): + raise Exception('bad compression') + self.proto.incomingCompression.decompress = stubDecompress + testBad('\x00\x00\x00\x04\x00BCDE', # bad decompression + transport.DISCONNECT_COMPRESSION_ERROR) + self.flushLoggedErrors() + + + def test_unimplementedPackets(self): + """ + Test that unimplemented packet types cause MSG_UNIMPLEMENTED packets + to be sent. + """ + seqnum = self.proto.incomingPacketSequence + def checkUnimplemented(seqnum=seqnum): + self.assertEquals(self.packets[0][0], + transport.MSG_UNIMPLEMENTED) + self.assertEquals(self.packets[0][1][3], chr(seqnum)) + self.proto.packets = [] + seqnum += 1 + + self.proto.dispatchMessage(40, '') + checkUnimplemented() + transport.messages[41] = 'MSG_fiction' + self.proto.dispatchMessage(41, '') + checkUnimplemented() + self.proto.dispatchMessage(60, '') + checkUnimplemented() + self.proto.setService(MockService()) + self.proto.dispatchMessage(70, '') + checkUnimplemented() + self.proto.dispatchMessage(71, '') + checkUnimplemented() + + + def test_getKey(self): + """ + Test that _getKey generates the correct keys. + """ + self.proto.sessionID = 'EF' + + k1 = sha.new('AB' + 'CD' + + 'K' + self.proto.sessionID).digest() + k2 = sha.new('ABCD' + k1).digest() + self.assertEquals(self.proto._getKey('K', 'AB', 'CD'), k1 + k2) + + + def test_multipleClasses(self): + """ + Test that multiple instances have distinct states. + """ + proto = self.proto + proto.dataReceived(self.transport.value()) + proto.currentEncryptions = MockCipher() + proto.outgoingCompression = MockCompression() + proto.incomingCompression = MockCompression() + proto.setService(MockService()) + proto2 = MockTransportBase() + proto2.makeConnection(proto_helpers.StringTransport()) + proto2.sendIgnore('') + self.failIfEquals(proto.gotVersion, proto2.gotVersion) + self.failIfEquals(proto.transport, proto2.transport) + self.failIfEquals(proto.outgoingPacketSequence, + proto2.outgoingPacketSequence) + self.failIfEquals(proto.incomingPacketSequence, + proto2.incomingPacketSequence) + self.failIfEquals(proto.currentEncryptions, + proto2.currentEncryptions) + self.failIfEquals(proto.service, proto2.service) + + + +class ServerAndClientSSHTransportBaseCase: + """ + Tests that need to be run on both the server and the client. + """ + + + def checkDisconnected(self, kind=None): + """ + Helper function to check if the transport disconnected. + """ + if kind is None: + kind = transport.DISCONNECT_PROTOCOL_ERROR + self.assertEquals(self.packets[-1][0], transport.MSG_DISCONNECT) + self.assertEquals(self.packets[-1][1][3], chr(kind)) + + + def connectModifiedProtocol(self, protoModification, + kind=None): + """ + Helper function to connect a modified protocol to the test protocol + and test for disconnection. + """ + if kind is None: + kind = transport.DISCONNECT_KEY_EXCHANGE_FAILED + proto2 = self.klass() + protoModification(proto2) + proto2.makeConnection(proto_helpers.StringTransport()) + self.proto.dataReceived(proto2.transport.value()) + if kind: + self.checkDisconnected(kind) + return proto2 + + + def test_disconnectIfCantMatchKex(self): + """ + Test that the transport disconnects if it can't match the key + exchange + """ + def blankKeyExchanges(proto2): + proto2.supportedKeyExchanges = [] + self.connectModifiedProtocol(blankKeyExchanges) + + + def test_disconnectIfCantMatchKeyAlg(self): + """ + Like test_disconnectIfCantMatchKex, but for the key algorithm. + """ + def blankPublicKeys(proto2): + proto2.supportedPublicKeys = [] + self.connectModifiedProtocol(blankPublicKeys) + + + def test_disconnectIfCantMatchCompression(self): + """ + Like test_disconnectIfCantMatchKex, but for the compression. + """ + def blankCompressions(proto2): + proto2.supportedCompressions = [] + self.connectModifiedProtocol(blankCompressions) + + + def test_disconnectIfCantMatchCipher(self): + """ + Like test_disconnectIfCantMatchKex, but for the encryption. + """ + def blankCiphers(proto2): + proto2.supportedCiphers = [] + self.connectModifiedProtocol(blankCiphers) + + + def test_disconnectIfCantMatchMAC(self): + """ + Like test_disconnectIfCantMatchKex, but for the MAC. + """ + def blankMACs(proto2): + proto2.supportedMACs = [] + self.connectModifiedProtocol(blankMACs) + + + +class ServerSSHTransportTestCase(ServerAndClientSSHTransportBaseCase, + TransportTestCase): + """ + Tests for the SSHServerTransport. + """ + + klass = transport.SSHServerTransport + + + def setUp(self): + TransportTestCase.setUp(self) + self.proto.factory = MockFactory() + self.proto.factory.startFactory() + + + def tearDown(self): + TransportTestCase.tearDown(self) + self.proto.factory.stopFactory() + del self.proto.factory + + + def test_KEXINIT(self): + """ + Test that receiving a KEXINIT packet sets up the correct values on the + server. + """ + self.proto.dataReceived( 'SSH-2.0-Twisted\r\n\x00\x00\x01\xd4\t\x14' + '\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99' + '\x99\x00\x00\x00=diffie-hellman-group1-sha1,diffie-hellman-g' + 'roup-exchange-sha1\x00\x00\x00\x0fssh-dss,ssh-rsa\x00\x00\x00' + '\x85aes128-ctr,aes128-cbc,aes192-ctr,aes192-cbc,aes256-ctr,ae' + 's256-cbc,cast128-ctr,cast128-cbc,blowfish-ctr,blowfish-cbc,3d' + 'es-ctr,3des-cbc\x00\x00\x00\x85aes128-ctr,aes128-cbc,aes192-c' + 'tr,aes192-cbc,aes256-ctr,aes256-cbc,cast128-ctr,cast128-cbc,b' + 'lowfish-ctr,blowfish-cbc,3des-ctr,3des-cbc\x00\x00\x00\x12hma' + 'c-md5,hmac-sha1\x00\x00\x00\x12hmac-md5,hmac-sha1\x00\x00\x00' + '\tnone,zlib\x00\x00\x00\tnone,zlib\x00\x00\x00\x00\x00\x00' + '\x00\x00\x00\x00\x00\x00\x00\x99\x99\x99\x99\x99\x99\x99\x99' + '\x99') + self.assertEquals(self.proto.kexAlg, + 'diffie-hellman-group1-sha1') + self.assertEquals(self.proto.keyAlg, + 'ssh-dss') + self.assertEquals(self.proto.outgoingCompressionType, + 'none') + self.assertEquals(self.proto.incomingCompressionType, + 'none') + ne = self.proto.nextEncryptions + self.assertEquals(ne.outCipType, 'aes128-ctr') + self.assertEquals(ne.inCipType, 'aes128-ctr') + self.assertEquals(ne.outMACType, 'hmac-md5') + self.assertEquals(ne.inMACType, 'hmac-md5') + + + def test_ignoreGuessPacketKex(self): + """ + The client is allowed to send a guessed key exchange packet + after it sends the KEXINIT packet. However, if the key exchanges + do not match, that guess packet must be ignored. This tests that + the packet is ignored in the case of the key exchange method not + matching. + """ + kexInitPacket = '\x00' * 16 + ( + ''.join([common.NS(x) for x in + [','.join(y) for y in + [self.proto.supportedKeyExchanges[::-1], + self.proto.supportedPublicKeys, + self.proto.supportedCiphers, + self.proto.supportedCiphers, + self.proto.supportedMACs, + self.proto.supportedMACs, + self.proto.supportedCompressions, + self.proto.supportedCompressions, + self.proto.supportedLanguages, + self.proto.supportedLanguages]]])) + ( + '\xff\x00\x00\x00\x00') + self.proto.ssh_KEXINIT(kexInitPacket) + self.assertTrue(self.proto.ignoreNextPacket) + self.proto.ssh_DEBUG("\x01\x00\x00\x00\x04test\x00\x00\x00\x00") + self.assertTrue(self.proto.ignoreNextPacket) + + + self.proto.ssh_KEX_DH_GEX_REQUEST_OLD('\x00\x00\x08\x00') + self.assertFalse(self.proto.ignoreNextPacket) + self.assertEquals(self.packets, []) + self.proto.ignoreNextPacket = True + + self.proto.ssh_KEX_DH_GEX_REQUEST('\x00\x00\x08\x00' * 3) + self.assertFalse(self.proto.ignoreNextPacket) + self.assertEquals(self.packets, []) + + + def test_ignoreGuessPacketKey(self): + """ + Like test_ignoreGuessPacketKex, but for an incorrectly guessed + public key format. + """ + kexInitPacket = '\x00' * 16 + ( + ''.join([common.NS(x) for x in + [','.join(y) for y in + [self.proto.supportedKeyExchanges, + self.proto.supportedPublicKeys[::-1], + self.proto.supportedCiphers, + self.proto.supportedCiphers, + self.proto.supportedMACs, + self.proto.supportedMACs, + self.proto.supportedCompressions, + self.proto.supportedCompressions, + self.proto.supportedLanguages, + self.proto.supportedLanguages]]])) + ( + '\xff\x00\x00\x00\x00') + self.proto.ssh_KEXINIT(kexInitPacket) + self.assertTrue(self.proto.ignoreNextPacket) + self.proto.ssh_DEBUG("\x01\x00\x00\x00\x04test\x00\x00\x00\x00") + self.assertTrue(self.proto.ignoreNextPacket) + + self.proto.ssh_KEX_DH_GEX_REQUEST_OLD('\x00\x00\x08\x00') + self.assertFalse(self.proto.ignoreNextPacket) + self.assertEquals(self.packets, []) + self.proto.ignoreNextPacket = True + + self.proto.ssh_KEX_DH_GEX_REQUEST('\x00\x00\x08\x00' * 3) + self.assertFalse(self.proto.ignoreNextPacket) + self.assertEquals(self.packets, []) + + + def test_KEXDH_INIT(self): + """ + Test that the KEXDH_INIT packet causes the server to send a + KEXDH_REPLY with the server's public key and a signature. + """ + self.proto.supportedKeyExchanges = ['diffie-hellman-group1-sha1'] + self.proto.supportedPublicKeys = ['ssh-rsa'] + self.proto.dataReceived(self.transport.value()) + e = pow(transport.DH_GENERATOR, 5000, + transport.DH_PRIME) + + self.proto.ssh_KEX_DH_GEX_REQUEST_OLD(common.MP(e)) + y = common.getMP('\x00\x00\x00\x40' + '\x99' * 64)[0] + f = common._MPpow(transport.DH_GENERATOR, y, transport.DH_PRIME) + sharedSecret = common._MPpow(e, y, transport.DH_PRIME) + + h = sha.new() + h.update(common.NS(self.proto.ourVersionString) * 2) + h.update(common.NS(self.proto.ourKexInitPayload) * 2) + h.update(common.NS(self.proto.factory.publicKeys['ssh-rsa'].blob())) + h.update(common.MP(e)) + h.update(f) + h.update(sharedSecret) + exchangeHash = h.digest() + + signature = self.proto.factory.privateKeys['ssh-rsa'].sign( + exchangeHash) + + self.assertEquals( + self.packets, + [(transport.MSG_KEXDH_REPLY, + common.NS(self.proto.factory.publicKeys['ssh-rsa'].blob()) + + f + common.NS(signature)), + (transport.MSG_NEWKEYS, '')]) + + + def test_KEX_DH_GEX_REQUEST_OLD(self): + """ + Test that the KEX_DH_GEX_REQUEST_OLD message causes the server + to reply with a KEX_DH_GEX_GROUP message with the correct + Diffie-Hellman group. + """ + self.proto.supportedKeyExchanges = [ + 'diffie-hellman-group-exchange-sha1'] + self.proto.supportedPublicKeys = ['ssh-rsa'] + self.proto.dataReceived(self.transport.value()) + self.proto.ssh_KEX_DH_GEX_REQUEST_OLD('\x00\x00\x04\x00') + self.assertEquals( + self.packets, + [(transport.MSG_KEX_DH_GEX_GROUP, + common.MP(transport.DH_PRIME) + '\x00\x00\x00\x01\x02')]) + self.assertEquals(self.proto.g, 2) + self.assertEquals(self.proto.p, transport.DH_PRIME) + + + def test_KEX_DH_GEX_REQUEST_OLD_badKexAlg(self): + """ + Test that if the server recieves a KEX_DH_GEX_REQUEST_OLD message + and the key exchange algorithm is not 'diffie-hellman-group1-sha1' or + 'diffie-hellman-group-exchange-sha1', we raise a ConchError. + """ + self.proto.kexAlg = None + self.assertRaises(ConchError, self.proto.ssh_KEX_DH_GEX_REQUEST_OLD, + None) + + + def test_KEX_DH_GEX_REQUEST(self): + """ + Test that the KEX_DH_GEX_REQUEST message causes the server to reply + with a KEX_DH_GEX_GROUP message with the correct Diffie-Hellman + group. + """ + self.proto.supportedKeyExchanges = [ + 'diffie-hellman-group-exchange-sha1'] + self.proto.supportedPublicKeys = ['ssh-rsa'] + self.proto.dataReceived(self.transport.value()) + self.proto.ssh_KEX_DH_GEX_REQUEST('\x00\x00\x04\x00\x00\x00\x08\x00' + + '\x00\x00\x0c\x00') + self.assertEquals( + self.packets, + [(transport.MSG_KEX_DH_GEX_GROUP, + common.MP(transport.DH_PRIME) + '\x00\x00\x00\x01\x03')]) + self.assertEquals(self.proto.g, 3) + self.assertEquals(self.proto.p, transport.DH_PRIME) + + + def test_KEX_DH_GEX_INIT_after_REQUEST(self): + """ + Test that the KEX_DH_GEX_INIT message after the client sends + KEX_DH_GEX_REQUEST causes the server to send a KEX_DH_GEX_INIT message + with a public key and signature. + """ + self.test_KEX_DH_GEX_REQUEST() + e = pow(self.proto.g, 3, self.proto.p) + y = common.getMP('\x00\x00\x00\x80' + '\x99' * 128)[0] + f = common._MPpow(self.proto.g, y, self.proto.p) + sharedSecret = common._MPpow(e, y, self.proto.p) + h = sha.new() + h.update(common.NS(self.proto.ourVersionString) * 2) + h.update(common.NS(self.proto.ourKexInitPayload) * 2) + h.update(common.NS(self.proto.factory.publicKeys['ssh-rsa'].blob())) + h.update('\x00\x00\x04\x00\x00\x00\x08\x00\x00\x00\x0c\x00') + h.update(common.MP(self.proto.p)) + h.update(common.MP(self.proto.g)) + h.update(common.MP(e)) + h.update(f) + h.update(sharedSecret) + exchangeHash = h.digest() + self.proto.ssh_KEX_DH_GEX_INIT(common.MP(e)) + self.assertEquals( + self.packets[1], + (transport.MSG_KEX_DH_GEX_REPLY, + common.NS(self.proto.factory.publicKeys['ssh-rsa'].blob()) + + f + common.NS(self.proto.factory.privateKeys['ssh-rsa'].sign( + exchangeHash)))) + + + def test_KEX_DH_GEX_INIT_after_REQUEST_OLD(self): + """ + Test that the KEX_DH_GEX_INIT message after the client sends + KEX_DH_GEX_REQUEST_OLD causes the server to sent a KEX_DH_GEX_INIT + message with a public key and signature. + """ + self.test_KEX_DH_GEX_REQUEST_OLD() + e = pow(self.proto.g, 3, self.proto.p) + y = common.getMP('\x00\x00\x00\x80' + '\x99' * 128)[0] + f = common._MPpow(self.proto.g, y, self.proto.p) + sharedSecret = common._MPpow(e, y, self.proto.p) + h = sha.new() + h.update(common.NS(self.proto.ourVersionString) * 2) + h.update(common.NS(self.proto.ourKexInitPayload) * 2) + h.update(common.NS(self.proto.factory.publicKeys['ssh-rsa'].blob())) + h.update('\x00\x00\x04\x00') + h.update(common.MP(self.proto.p)) + h.update(common.MP(self.proto.g)) + h.update(common.MP(e)) + h.update(f) + h.update(sharedSecret) + exchangeHash = h.digest() + self.proto.ssh_KEX_DH_GEX_INIT(common.MP(e)) + self.assertEquals( + self.packets[1:], + [(transport.MSG_KEX_DH_GEX_REPLY, + common.NS(self.proto.factory.publicKeys['ssh-rsa'].blob()) + + f + common.NS(self.proto.factory.privateKeys['ssh-rsa'].sign( + exchangeHash))), + (transport.MSG_NEWKEYS, '')]) + + + def test_keySetup(self): + """ + Test that _keySetup sets up the next encryption keys. + """ + self.proto.nextEncryptions = MockCipher() + self.proto._keySetup('AB', 'CD') + self.assertEquals(self.proto.sessionID, 'CD') + self.proto._keySetup('AB', 'EF') + self.assertEquals(self.proto.sessionID, 'CD') + self.assertEquals(self.packets[-1], (transport.MSG_NEWKEYS, '')) + newKeys = [self.proto._getKey(c, 'AB', 'EF') for c in 'ABCDEF'] + self.assertEquals( + self.proto.nextEncryptions.keys, + (newKeys[1], newKeys[3], newKeys[0], newKeys[2], newKeys[5], + newKeys[4])) + + + def test_NEWKEYS(self): + """ + Test that NEWKEYS transitions the keys in nextEncryptions to + currentEncryptions. + """ + self.test_KEXINIT() + + self.proto.nextEncryptions = transport.SSHCiphers('none', 'none', + 'none', 'none') + self.proto.ssh_NEWKEYS('') + self.assertIdentical(self.proto.currentEncryptions, + self.proto.nextEncryptions) + self.assertIdentical(self.proto.outgoingCompression, None) + self.assertIdentical(self.proto.incomingCompression, None) + self.proto.outgoingCompressionType = 'zlib' + self.proto.ssh_NEWKEYS('') + self.failIfIdentical(self.proto.outgoingCompression, None) + self.proto.incomingCompressionType = 'zlib' + self.proto.ssh_NEWKEYS('') + self.failIfIdentical(self.proto.incomingCompression, None) + + + def test_SERVICE_REQUEST(self): + """ + Test that the SERVICE_REQUEST message requests and starts a + service. + """ + self.proto.ssh_SERVICE_REQUEST(common.NS('ssh-userauth')) + self.assertEquals(self.packets, [(transport.MSG_SERVICE_ACCEPT, + common.NS('ssh-userauth'))]) + self.assertEquals(self.proto.service.name, 'MockService') + + + def test_disconnectNEWKEYSData(self): + """ + Test that NEWKEYS disconnects if it receives data. + """ + self.proto.ssh_NEWKEYS("bad packet") + self.checkDisconnected() + + + def test_disconnectSERVICE_REQUESTBadService(self): + """ + Test that SERVICE_REQUESTS disconnects if an unknown service is + requested. + """ + self.proto.ssh_SERVICE_REQUEST(common.NS('no service')) + self.checkDisconnected(transport.DISCONNECT_SERVICE_NOT_AVAILABLE) + + + +class ClientSSHTransportTestCase(ServerAndClientSSHTransportBaseCase, + TransportTestCase): + """ + Tests for SSHClientTransport. + """ + + klass = transport.SSHClientTransport + + + def test_KEXINIT(self): + """ + Test that receiving a KEXINIT packet sets up the correct values on the + client. The way algorithms are picks is that the first item in the + client's list that is also in the server's list is chosen. + """ + self.proto.dataReceived( 'SSH-2.0-Twisted\r\n\x00\x00\x01\xd4\t\x14' + '\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99' + '\x99\x00\x00\x00=diffie-hellman-group1-sha1,diffie-hellman-g' + 'roup-exchange-sha1\x00\x00\x00\x0fssh-dss,ssh-rsa\x00\x00\x00' + '\x85aes128-ctr,aes128-cbc,aes192-ctr,aes192-cbc,aes256-ctr,ae' + 's256-cbc,cast128-ctr,cast128-cbc,blowfish-ctr,blowfish-cbc,3d' + 'es-ctr,3des-cbc\x00\x00\x00\x85aes128-ctr,aes128-cbc,aes192-c' + 'tr,aes192-cbc,aes256-ctr,aes256-cbc,cast128-ctr,cast128-cbc,b' + 'lowfish-ctr,blowfish-cbc,3des-ctr,3des-cbc\x00\x00\x00\x12hma' + 'c-md5,hmac-sha1\x00\x00\x00\x12hmac-md5,hmac-sha1\x00\x00\x00' + '\tzlib,none\x00\x00\x00\tzlib,none\x00\x00\x00\x00\x00\x00' + '\x00\x00\x00\x00\x00\x00\x00\x99\x99\x99\x99\x99\x99\x99\x99' + '\x99') + self.assertEquals(self.proto.kexAlg, + 'diffie-hellman-group-exchange-sha1') + self.assertEquals(self.proto.keyAlg, + 'ssh-rsa') + self.assertEquals(self.proto.outgoingCompressionType, + 'none') + self.assertEquals(self.proto.incomingCompressionType, + 'none') + ne = self.proto.nextEncryptions + self.assertEquals(ne.outCipType, 'aes256-ctr') + self.assertEquals(ne.inCipType, 'aes256-ctr') + self.assertEquals(ne.outMACType, 'hmac-sha1') + self.assertEquals(ne.inMACType, 'hmac-sha1') + + + def verifyHostKey(self, pubKey, fingerprint): + """ + Mock version of SSHClientTransport.verifyHostKey. + """ + self.calledVerifyHostKey = True + self.assertEquals(pubKey, self.blob) + self.assertEquals(fingerprint.replace(':', ''), + md5.new(pubKey).hexdigest()) + return defer.succeed(True) + + + def setUp(self): + TransportTestCase.setUp(self) + self.blob = keys.Key.fromString(keydata.publicRSA_openssh).blob() + self.privObj = keys.Key.fromString(keydata.privateRSA_openssh) + self.calledVerifyHostKey = False + self.proto.verifyHostKey = self.verifyHostKey + + + def test_notImplementedClientMethods(self): + """ + verifyHostKey() should return a Deferred which fails with a + NotImplementedError exception. connectionSecure() should raise + NotImplementedError(). + """ + self.assertRaises(NotImplementedError, self.klass().connectionSecure) + def _checkRaises(f): + f.trap(NotImplementedError) + d = self.klass().verifyHostKey(None, None) + return d.addCallback(self.fail).addErrback(_checkRaises) + + + def test_KEXINIT_groupexchange(self): + """ + Test that a KEXINIT packet with a group-exchange key exchange results + in a KEX_DH_GEX_REQUEST_OLD message.. + """ + self.proto.supportedKeyExchanges = [ + 'diffie-hellman-group-exchange-sha1'] + self.proto.dataReceived(self.transport.value()) + self.assertEquals(self.packets, [(transport.MSG_KEX_DH_GEX_REQUEST_OLD, + '\x00\x00\x08\x00')]) + + + def test_KEXINIT_group1(self): + """ + Like test_KEXINIT_groupexchange, but for the group-1 key exchange. + """ + self.proto.supportedKeyExchanges = ['diffie-hellman-group1-sha1'] + self.proto.dataReceived(self.transport.value()) + self.assertEquals(common.MP(self.proto.x)[5:], '\x99' * 64) + self.assertEquals(self.packets, + [(transport.MSG_KEXDH_INIT, self.proto.e)]) + + + def test_KEXINIT_badKexAlg(self): + """ + Test that the client raises a ConchError if it receives a + KEXINIT message bug doesn't have a key exchange algorithm that we + understand. + """ + self.proto.supportedKeyExchanges = ['diffie-hellman-group2-sha1'] + data = self.transport.value().replace('group1', 'group2') + self.assertRaises(ConchError, self.proto.dataReceived, data) + + + def test_KEXDH_REPLY(self): + """ + Test that the KEXDH_REPLY message verifies the server. + """ + self.test_KEXINIT_group1() + + sharedSecret = common._MPpow(transport.DH_GENERATOR, + self.proto.x, transport.DH_PRIME) + h = sha.new() + h.update(common.NS(self.proto.ourVersionString) * 2) + h.update(common.NS(self.proto.ourKexInitPayload) * 2) + h.update(common.NS(self.blob)) + h.update(self.proto.e) + h.update('\x00\x00\x00\x01\x02') # f + h.update(sharedSecret) + exchangeHash = h.digest() + + def _cbTestKEXDH_REPLY(value): + self.assertIdentical(value, None) + self.assertEquals(self.calledVerifyHostKey, True) + self.assertEquals(self.proto.sessionID, exchangeHash) + + signature = self.privObj.sign(exchangeHash) + + d = self.proto.ssh_KEX_DH_GEX_GROUP( + (common.NS(self.blob) + '\x00\x00\x00\x01\x02' + + common.NS(signature))) + d.addCallback(_cbTestKEXDH_REPLY) + + return d + + + def test_KEX_DH_GEX_GROUP(self): + """ + Test that the KEX_DH_GEX_GROUP message results in a + KEX_DH_GEX_INIT message with the client's Diffie-Hellman public key. + """ + self.test_KEXINIT_groupexchange() + self.proto.ssh_KEX_DH_GEX_GROUP( + '\x00\x00\x00\x01\x0f\x00\x00\x00\x01\x02') + self.assertEquals(self.proto.p, 15) + self.assertEquals(self.proto.g, 2) + self.assertEquals(common.MP(self.proto.x)[5:], '\x99' * 40) + self.assertEquals(self.proto.e, + common.MP(pow(2, self.proto.x, 15))) + self.assertEquals(self.packets[1:], [(transport.MSG_KEX_DH_GEX_INIT, + self.proto.e)]) + + + def test_KEX_DH_GEX_REPLY(self): + """ + Test that the KEX_DH_GEX_REPLY message results in a verified + server. + """ + + self.test_KEX_DH_GEX_GROUP() + sharedSecret = common._MPpow(3, self.proto.x, self.proto.p) + h = sha.new() + h.update(common.NS(self.proto.ourVersionString) * 2) + h.update(common.NS(self.proto.ourKexInitPayload) * 2) + h.update(common.NS(self.blob)) + h.update('\x00\x00\x08\x00\x00\x00\x00\x01\x0f\x00\x00\x00\x01\x02') + h.update(self.proto.e) + h.update('\x00\x00\x00\x01\x03') # f + h.update(sharedSecret) + exchangeHash = h.digest() + + def _cbTestKEX_DH_GEX_REPLY(value): + self.assertIdentical(value, None) + self.assertEquals(self.calledVerifyHostKey, True) + self.assertEquals(self.proto.sessionID, exchangeHash) + + signature = self.privObj.sign(exchangeHash) + + d = self.proto.ssh_KEX_DH_GEX_REPLY( + common.NS(self.blob) + + '\x00\x00\x00\x01\x03' + + common.NS(signature)) + d.addCallback(_cbTestKEX_DH_GEX_REPLY) + return d + + + def test_keySetup(self): + """ + Test that _keySetup sets up the next encryption keys. + """ + self.proto.nextEncryptions = MockCipher() + self.proto._keySetup('AB', 'CD') + self.assertEquals(self.proto.sessionID, 'CD') + self.proto._keySetup('AB', 'EF') + self.assertEquals(self.proto.sessionID, 'CD') + self.assertEquals(self.packets[-1], (transport.MSG_NEWKEYS, '')) + newKeys = [self.proto._getKey(c, 'AB', 'EF') for c in 'ABCDEF'] + self.assertEquals(self.proto.nextEncryptions.keys, + (newKeys[0], newKeys[2], newKeys[1], newKeys[3], + newKeys[4], newKeys[5])) + + + def test_NEWKEYS(self): + """ + Test that NEWKEYS transitions the keys from nextEncryptions to + currentEncryptions. + """ + self.test_KEXINIT() + secure = [False] + def stubConnectionSecure(): + secure[0] = True + self.proto.connectionSecure = stubConnectionSecure + + self.proto.nextEncryptions = transport.SSHCiphers('none', 'none', + 'none', 'none') + self.proto.ssh_NEWKEYS('') + + self.failIfIdentical(self.proto.currentEncryptions, + self.proto.nextEncryptions) + + self.proto.nextEncryptions = MockCipher() + self.proto._keySetup('AB', 'EF') + self.assertIdentical(self.proto.outgoingCompression, None) + self.assertIdentical(self.proto.incomingCompression, None) + self.assertIdentical(self.proto.currentEncryptions, + self.proto.nextEncryptions) + self.assertTrue(secure[0]) + self.proto.outgoingCompressionType = 'zlib' + self.proto.ssh_NEWKEYS('') + self.failIfIdentical(self.proto.outgoingCompression, None) + self.proto.incomingCompressionType = 'zlib' + self.proto.ssh_NEWKEYS('') + self.failIfIdentical(self.proto.incomingCompression, None) + + + def test_SERVICE_ACCEPT(self): + """ + Test that the SERVICE_ACCEPT packet starts the requested service. + """ + self.proto.instance = MockService() + self.proto.ssh_SERVICE_ACCEPT('\x00\x00\x00\x0bMockService') + self.assertTrue(self.proto.instance.started) + + + def test_requestService(self): + """ + Test that requesting a service sends a SERVICE_REQUEST packet. + """ + self.proto.requestService(MockService()) + self.assertEquals(self.packets, [(transport.MSG_SERVICE_REQUEST, + '\x00\x00\x00\x0bMockService')]) + + + def test_disconnectKEXDH_REPLYBadSignature(self): + """ + Test that KEXDH_REPLY disconnects if the signature is bad. + """ + self.test_KEXDH_REPLY() + self.proto._continueKEXDH_REPLY(None, self.blob, 3, "bad signature") + self.checkDisconnected(transport.DISCONNECT_KEY_EXCHANGE_FAILED) + + + def test_disconnectGEX_REPLYBadSignature(self): + """ + Like test_disconnectKEXDH_REPLYBadSignature, but for DH_GEX_REPLY. + """ + self.test_KEX_DH_GEX_REPLY() + self.proto._continueGEX_REPLY(None, self.blob, 3, "bad signature") + self.checkDisconnected(transport.DISCONNECT_KEY_EXCHANGE_FAILED) + + + def test_disconnectNEWKEYSData(self): + """ + Test that NEWKEYS disconnects if it receives data. + """ + self.proto.ssh_NEWKEYS("bad packet") + self.checkDisconnected() + + + def test_disconnectSERVICE_ACCEPT(self): + """ + Test that SERVICE_ACCEPT disconnects if the accepted protocol is + differet from the asked-for protocol. + """ + self.proto.instance = MockService() + self.proto.ssh_SERVICE_ACCEPT('\x00\x00\x00\x03bad') + self.checkDisconnected() + + + +class SSHCiphersTestCase(unittest.TestCase): + """ + Tests for the SSHCiphers helper class. + """ + if Crypto is None: + skip = "cannot run w/o PyCrypto" + + + def test_init(self): + """ + Test that the initializer sets up the SSHCiphers object. + """ + ciphers = transport.SSHCiphers('A', 'B', 'C', 'D') + self.assertEquals(ciphers.outCipType, 'A') + self.assertEquals(ciphers.inCipType, 'B') + self.assertEquals(ciphers.outMACType, 'C') + self.assertEquals(ciphers.inMACType, 'D') + + + def test_getCipher(self): + """ + Test that the _getCipher method returns the correct cipher. + """ + ciphers = transport.SSHCiphers('A', 'B', 'C', 'D') + iv = key = '\x00' * 16 + for cipName, (modName, keySize, counter) in ciphers.cipherMap.items(): + cip = ciphers._getCipher(cipName, iv, key) + if cipName == 'none': + self.assertIsInstance(cip, transport._DummyCipher) + else: + self.assertTrue(str(cip).startswith('<' + modName)) + + + def test_getMAC(self): + """ + Test that the _getMAC method returns the correct MAC. + """ + ciphers = transport.SSHCiphers('A', 'B', 'C', 'D') + key = '\x00' * 64 + for macName, mac in ciphers.macMap.items(): + mod = ciphers._getMAC(macName, key) + if macName == 'none': + self.assertIdentical(mac, None) + else: + self.assertEquals(mod[0], mac) + self.assertEquals(mod[1], + Crypto.Cipher.XOR.new('\x36').encrypt(key)) + self.assertEquals(mod[2], + Crypto.Cipher.XOR.new('\x5c').encrypt(key)) + self.assertEquals(mod[3], len(mod[0].new().digest())) + + + def test_setKeysCiphers(self): + """ + Test that setKeys sets up the ciphers. + """ + key = '\x00' * 64 + cipherItems = transport.SSHCiphers.cipherMap.items() + for cipName, (modName, keySize, counter) in cipherItems: + encCipher = transport.SSHCiphers(cipName, 'none', 'none', 'none') + decCipher = transport.SSHCiphers('none', cipName, 'none', 'none') + cip = encCipher._getCipher(cipName, key, key) + bs = cip.block_size + encCipher.setKeys(key, key, '', '', '', '') + decCipher.setKeys('', '', key, key, '', '') + self.assertEquals(encCipher.encBlockSize, bs) + self.assertEquals(decCipher.decBlockSize, bs) + enc = cip.encrypt(key[:bs]) + enc2 = cip.encrypt(key[:bs]) + if counter: + self.failIfEquals(enc, enc2) + self.assertEquals(encCipher.encrypt(key[:bs]), enc) + self.assertEquals(encCipher.encrypt(key[:bs]), enc2) + self.assertEquals(decCipher.decrypt(enc), key[:bs]) + self.assertEquals(decCipher.decrypt(enc2), key[:bs]) + + + def test_setKeysMACs(self): + """ + Test that setKeys sets up the MACs. + """ + key = '\x00' * 64 + for macName, mod in transport.SSHCiphers.macMap.items(): + outMac = transport.SSHCiphers('none', 'none', macName, 'none') + inMac = transport.SSHCiphers('none', 'none', 'none', macName) + outMac.setKeys('', '', '', '', key, '') + inMac.setKeys('', '', '', '', '', key) + if mod: + ds = mod.digest_size + else: + ds = 0 + self.assertEquals(inMac.verifyDigestSize, ds) + if mod: + mod, i, o, ds = outMac._getMAC(macName, key) + seqid = 0 + data = key + packet = '\x00' * 4 + key + if mod: + mac = mod.new(o + mod.new(i + packet).digest()).digest() + else: + mac = '' + self.assertEquals(outMac.makeMAC(seqid, data), mac) + self.assertTrue(inMac.verify(seqid, data, mac)) + + + +class CounterTestCase(unittest.TestCase): + """ + Tests for the _Counter helper class. + """ + if Crypto is None: + skip = "cannot run w/o PyCrypto" + + + def test_init(self): + """ + Test that the counter is initialized correctly. + """ + counter = transport._Counter('\x00' * 8 + '\xff' * 8, 8) + self.assertEquals(counter.blockSize, 8) + self.assertEquals(counter.count.tostring(), '\x00' * 8) + + + def test_count(self): + """ + Test that the counter counts incrementally and wraps at the top. + """ + counter = transport._Counter('\x00', 1) + self.assertEquals(counter(), '\x01') + self.assertEquals(counter(), '\x02') + [counter() for i in range(252)] + self.assertEquals(counter(), '\xff') + self.assertEquals(counter(), '\x00') + + + +class TransportLoopbackTestCase(unittest.TestCase): + """ + Test the server transport and client transport against each other, + """ + if Crypto is None: + skip = "cannot run w/o PyCrypto" + + + def _runClientServer(self, mod): + """ + Run an async client and server, modifying each using the mod function + provided. Returns a Deferred called back when both Protocols have + disconnected. + + @type mod: C{func} + @rtype: C{defer.Deferred} + """ + factory = MockFactory() + server = transport.SSHServerTransport() + server.factory = factory + factory.startFactory() + server.errors = [] + server.receiveError = lambda code, desc: server.errors.append(( + code, desc)) + client = transport.SSHClientTransport() + client.verifyHostKey = lambda x, y: defer.succeed(None) + client.errors = [] + client.receiveError = lambda code, desc: client.errors.append(( + code, desc)) + client.connectionSecure = lambda: client.loseConnection() + server = mod(server) + client = mod(client) + def check(ignored, server, client): + name = repr([server.supportedCiphers[0], + server.supportedMACs[0], + server.supportedKeyExchanges[0], + server.supportedCompressions[0]]) + self.assertEquals(client.errors, []) + self.assertEquals(server.errors, [( + transport.DISCONNECT_CONNECTION_LOST, + "user closed connection")]) + if server.supportedCiphers[0] == 'none': + self.assertFalse(server.isEncrypted(), name) + self.assertFalse(client.isEncrypted(), name) + else: + self.assertTrue(server.isEncrypted(), name) + self.assertTrue(client.isEncrypted(), name) + if server.supportedMACs[0] == 'none': + self.assertFalse(server.isVerified(), name) + self.assertFalse(client.isVerified(), name) + else: + self.assertTrue(server.isVerified(), name) + self.assertTrue(client.isVerified(), name) + + d = loopback.loopbackAsync(server, client) + d.addCallback(check, server, client) + return d + + + def test_ciphers(self): + """ + Test that the client and server play nicely together, in all + the various combinations of ciphers. + """ + deferreds = [] + for cipher in transport.SSHTransportBase.supportedCiphers + ['none']: + def setCipher(proto): + proto.supportedCiphers = [cipher] + return proto + deferreds.append(self._runClientServer(setCipher)) + return defer.DeferredList(deferreds, fireOnOneErrback=True) + + + def test_macs(self): + """ + Like test_ciphers, but for the various MACs. + """ + deferreds = [] + for mac in transport.SSHTransportBase.supportedMACs + ['none']: + def setMAC(proto): + proto.supportedMACs = [mac] + return proto + deferreds.append(self._runClientServer(setMAC)) + return defer.DeferredList(deferreds, fireOnOneErrback=True) + + + def test_keyexchanges(self): + """ + Like test_ciphers, but for the various key exchanges. + """ + deferreds = [] + for kex in transport.SSHTransportBase.supportedKeyExchanges: + def setKeyExchange(proto): + proto.supportedKeyExchanges = [kex] + return proto + deferreds.append(self._runClientServer(setKeyExchange)) + return defer.DeferredList(deferreds, fireOnOneErrback=True) + + + def test_compressions(self): + """ + Like test_ciphers, but for the various compressions. + """ + deferreds = [] + for compression in transport.SSHTransportBase.supportedCompressions: + def setCompression(proto): + proto.supportedCompressions = [compression] + return proto + deferreds.append(self._runClientServer(setCompression)) + return defer.DeferredList(deferreds, fireOnOneErrback=True) + + + +class OldFactoryTestCase(unittest.TestCase): + """ + The old C{SSHFactory.getPublicKeys}() returned mappings of key names to + strings of key blobs and mappings of key names to PyCrypto key objects from + C{SSHFactory.getPrivateKeys}() (they could also be specified with the + C{publicKeys} and C{privateKeys} attributes). This is no longer supported + by the C{SSHServerTransport}, so we warn the user if they create an old + factory. + """ + + if Crypto is None: + skip = "cannot run w/o PyCrypto" + + + def test_getPublicKeysWarning(self): + """ + If the return value of C{getPublicKeys}() isn't a mapping from key + names to C{Key} objects, then warn the user and convert the mapping. + """ + sshFactory = MockOldFactoryPublicKeys() + self.assertWarns(DeprecationWarning, + "Returning a mapping from strings to strings from" + " getPublicKeys()/publicKeys (in %s) is deprecated. Return " + "a mapping from strings to Key objects instead." % + (qual(MockOldFactoryPublicKeys),), + factory.__file__, sshFactory.startFactory) + self.assertEquals(sshFactory.publicKeys, MockFactory().getPublicKeys()) + + + def test_getPrivateKeysWarning(self): + """ + If the return value of C{getPrivateKeys}() isn't a mapping from key + names to C{Key} objects, then warn the user and convert the mapping. + """ + sshFactory = MockOldFactoryPrivateKeys() + self.assertWarns(DeprecationWarning, + "Returning a mapping from strings to PyCrypto key objects from" + " getPrivateKeys()/privateKeys (in %s) is deprecated. Return" + " a mapping from strings to Key objects instead." % + (qual(MockOldFactoryPrivateKeys),), + factory.__file__, sshFactory.startFactory) + self.assertEquals(sshFactory.privateKeys, + MockFactory().getPrivateKeys()) + + + def test_publicKeysWarning(self): + """ + If the value of the C{publicKeys} attribute isn't a mapping from key + names to C{Key} objects, then warn the user and convert the mapping. + """ + sshFactory = MockOldFactoryPublicKeys() + sshFactory.publicKeys = sshFactory.getPublicKeys() + self.assertWarns(DeprecationWarning, + "Returning a mapping from strings to strings from" + " getPublicKeys()/publicKeys (in %s) is deprecated. Return " + "a mapping from strings to Key objects instead." % + (qual(MockOldFactoryPublicKeys),), + factory.__file__, sshFactory.startFactory) + self.assertEquals(sshFactory.publicKeys, MockFactory().getPublicKeys()) + + + def test_privateKeysWarning(self): + """ + If the return value of C{privateKeys} attribute isn't a mapping from + key names to C{Key} objects, then warn the user and convert the + mapping. + """ + sshFactory = MockOldFactoryPrivateKeys() + sshFactory.privateKeys = sshFactory.getPrivateKeys() + self.assertWarns(DeprecationWarning, + "Returning a mapping from strings to PyCrypto key objects from" + " getPrivateKeys()/privateKeys (in %s) is deprecated. Return" + " a mapping from strings to Key objects instead." % + (qual(MockOldFactoryPrivateKeys),), + factory.__file__, sshFactory.startFactory) + self.assertEquals(sshFactory.privateKeys, + MockFactory().getPrivateKeys()) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_userauth.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_userauth.py --- twisted-conch-0.8.0/twisted/conch/test/test_userauth.py 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_userauth.py 2008-09-10 15:58:45.000000000 +0100 @@ -0,0 +1,188 @@ + +from zope.interface import implements + +from twisted.cred.checkers import ICredentialsChecker +from twisted.cred.credentials import IUsernamePassword +from twisted.cred.error import UnauthorizedLogin +from twisted.cred.portal import IRealm, Portal + +try: + import Crypto.Cipher.DES3, Crypto.Cipher.XOR +except ImportError: + userauth = transport = None +else: + from twisted.conch.ssh import userauth, transport + +from twisted.conch.error import ConchError +from twisted.conch.ssh.common import NS + +from twisted.internet import defer + +from twisted.trial import unittest + + + +if transport is not None: + class FakeTransport(transport.SSHServerTransport): + """ + L{userauth.SSHUserAuthServer} expects an SSH transport which has a factory + attribute which has a portal attribute. Because the portal is important for + testing authentication, we need to be able to provide an interesting portal + object to the C{SSHUserAuthServer}. + + In addition, we want to be able to capture any packets sent over the + transport. + """ + + + class Service(object): + name = 'nancy' + + def serviceStarted(self): + pass + + + class Factory(object): + def _makeService(self): + return FakeTransport.Service() + + def getService(self, transport, nextService): + # This has to return a callable. + return self._makeService + + + def __init__(self, portal): + self.factory = self.Factory() + self.factory.portal = portal + self.packets = [] + + + def sendPacket(self, messageType, message): + self.packets.append((messageType, message)) + + + def isEncrypted(self, direction): + """ + Pretend that this transport encrypts traffic in both directions. The + SSHUserAuthServer disables password authentication if the transport + isn't encrypted. + """ + return True + + + +class Realm(object): + """ + A mock realm for testing L{userauth.SSHUserAuthServer}. + + This realm is not actually used in the course of testing, so it returns the + simplest thing that could possibly work. + """ + + implements(IRealm) + + def requestAvatar(self, avatarId, mind, *interfaces): + return defer.succeed((interfaces[0], None, lambda: None)) + + + +class MockChecker(object): + """ + A very simple username/password checker which authenticates anyone whose + password matches their username and rejects all others. + """ + + credentialInterfaces = (IUsernamePassword,) + implements(ICredentialsChecker) + + + def requestAvatarId(self, creds): + if creds.username == creds.password: + return defer.succeed(creds.username) + return defer.fail(UnauthorizedLogin("Invalid username/password pair")) + + + +class TestSSHUserAuthServer(unittest.TestCase): + """ + Tests for SSHUserAuthServer. + """ + + if userauth is None: + skip = "Cannot run without PyCrypto" + + def setUp(self): + self.realm = Realm() + portal = Portal(self.realm) + portal.registerChecker(MockChecker()) + self.authServer = userauth.SSHUserAuthServer() + self.authServer.transport = FakeTransport(portal) + self.authServer.serviceStarted() + + + def tearDown(self): + self.authServer.serviceStopped() + self.authServer = None + + + def test_successfulAuthentication(self): + """ + When provided with correct authentication information, the server + should respond by sending a MSG_USERAUTH_SUCCESS message with no other + data. + + See RFC 4252, Section 5.1. + """ + packet = NS('foo') + NS('none') + NS('password') + chr(0) + NS('foo') + d = self.authServer.ssh_USERAUTH_REQUEST(packet) + + def check(ignored): + # Check that the server reports the failure, including 'password' + # as a valid authentication type. + self.assertEqual( + self.authServer.transport.packets, + [(userauth.MSG_USERAUTH_SUCCESS, '')]) + return d.addCallback(check) + + + def test_failedAuthentication(self): + """ + When provided with invalid authentication details, the server should + respond by sending a MSG_USERAUTH_FAILURE message which states whether + the authentication was partially successful, and provides other, open + options for authentication. + + See RFC 4252, Section 5.1. + """ + # packet = username, next_service, authentication type, FALSE, password + packet = NS('foo') + NS('none') + NS('password') + chr(0) + NS('bar') + d = self.authServer.ssh_USERAUTH_REQUEST(packet) + + def check(ignored): + # Check that the server reports the failure, including 'password' + # as a valid authentication type. + self.assertEqual( + self.authServer.transport.packets, + [(userauth.MSG_USERAUTH_FAILURE, NS('password') + chr(0))]) + return d.addCallback(check) + + + def test_requestRaisesConchError(self): + """ + ssh_USERAUTH_REQUEST should raise a ConchError if tryAuth returns + None. Added to catch a bug noticed by pyflakes. This is a whitebox + test. + """ + def mockTryAuth(kind, user, data): + return None + + def mockEbBadAuth(reason): + reason.trap(ConchError) + + self.patch(self.authServer, 'tryAuth', mockTryAuth) + self.patch(self.authServer, '_ebBadAuth', mockEbBadAuth) + + packet = NS('user') + NS('none') + NS('public-key') + NS('data') + # If an error other than ConchError is raised, this will trigger an + # exception. + return self.authServer.ssh_USERAUTH_REQUEST(packet) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/test/test_window.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/test/test_window.py --- twisted-conch-0.8.0/twisted/conch/test/test_window.py 1970-01-01 01:00:00.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/test/test_window.py 2007-01-22 22:01:11.000000000 +0000 @@ -0,0 +1,49 @@ + +""" +Tests for the insults windowing module, L{twisted.conch.insults.window}. +""" + +from twisted.trial.unittest import TestCase + +from twisted.conch.insults.window import TopWindow + + +class TopWindowTests(TestCase): + """ + Tests for L{TopWindow}, the root window container class. + """ + + def test_paintScheduling(self): + """ + Verify that L{TopWindow.repaint} schedules an actual paint to occur + using the scheduling object passed to its initializer. + """ + paints = [] + scheduled = [] + root = TopWindow(lambda: paints.append(None), scheduled.append) + + # Nothing should have happened yet. + self.assertEqual(paints, []) + self.assertEqual(scheduled, []) + + # Cause a paint to be scheduled. + root.repaint() + self.assertEqual(paints, []) + self.assertEqual(len(scheduled), 1) + + # Do another one to verify nothing else happens as long as the previous + # one is still pending. + root.repaint() + self.assertEqual(paints, []) + self.assertEqual(len(scheduled), 1) + + # Run the actual paint call. + scheduled.pop()() + self.assertEqual(len(paints), 1) + self.assertEqual(scheduled, []) + + # Do one more to verify that now that the previous one is finished + # future paints will succeed. + root.repaint() + self.assertEqual(len(paints), 1) + self.assertEqual(len(scheduled), 1) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ui/ansi.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ui/ansi.py --- twisted-conch-0.8.0/twisted/conch/ui/ansi.py 2006-07-23 13:43:16.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ui/ansi.py 2008-07-29 21:13:54.000000000 +0100 @@ -4,7 +4,7 @@ # """Module to parse ANSI escape sequences -Maintainer: U{Jean-Paul Calderone } +Maintainer: Jean-Paul Calderone """ import string diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ui/__init__.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ui/__init__.py --- twisted-conch-0.8.0/twisted/conch/ui/__init__.py 2004-08-25 09:36:30.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ui/__init__.py 2008-07-29 21:13:54.000000000 +0100 @@ -4,9 +4,8 @@ # -"""twisted.conch.ui is home to the UI elements for tkconch. - -This module is unstable. +""" +twisted.conch.ui is home to the UI elements for tkconch. -Maintainer: U{Paul Swartz} +Maintainer: Paul Swartz """ diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/ui/tkvt100.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/ui/tkvt100.py --- twisted-conch-0.8.0/twisted/conch/ui/tkvt100.py 2005-08-21 14:31:20.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/ui/tkvt100.py 2008-07-29 21:13:54.000000000 +0100 @@ -5,7 +5,7 @@ """Module to emulate a VT100 terminal in Tkinter. -Maintainer: U{Paul Swartz } +Maintainer: Paul Swartz """ import Tkinter, tkFont diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/unix.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/unix.py --- twisted-conch-0.8.0/twisted/conch/unix.py 2006-07-01 05:36:54.000000000 +0100 +++ twisted-conch-8.2.0/twisted/conch/unix.py 2007-03-11 19:38:35.000000000 +0000 @@ -1,11 +1,9 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2007 Twisted Matrix Laboratories. # See LICENSE for details. -# - from twisted.cred import portal from twisted.python import components, log -from twisted.internet.process import ProcessExitedAlready +from twisted.internet.error import ProcessExitedAlready from zope import interface from ssh import session, forwarding, filetransfer from ssh.filetransfer import FXF_READ, FXF_WRITE, FXF_APPEND, FXF_CREAT, FXF_TRUNC, FXF_EXCL diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/conch/_version.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/conch/_version.py --- twisted-conch-0.8.0/twisted/conch/_version.py 2007-01-06 19:55:11.000000000 +0000 +++ twisted-conch-8.2.0/twisted/conch/_version.py 2008-12-17 04:37:54.000000000 +0000 @@ -1,3 +1,3 @@ -# This is an auto-generated file. Use admin/change-versions to update. +# This is an auto-generated file. Do not edit it. from twisted.python import versions -version = versions.Version(__name__[:__name__.rfind('.')], 0, 8, 0) +version = versions.Version('twisted.conch', 8, 2, 0) diff -Nru /tmp/DpoPumTT3Y/twisted-conch-0.8.0/twisted/plugins/twisted_conch.py /tmp/zqzOGlY6tR/twisted-conch-8.2.0/twisted/plugins/twisted_conch.py --- twisted-conch-0.8.0/twisted/plugins/twisted_conch.py 2005-03-22 07:24:47.000000000 +0000 +++ twisted-conch-8.2.0/twisted/plugins/twisted_conch.py 2008-04-08 16:53:14.000000000 +0100 @@ -1,15 +1,15 @@ -# Copyright (c) 2001-2004 Twisted Matrix Laboratories. +# Copyright (c) 2001-2008 Twisted Matrix Laboratories. # See LICENSE for details. -from twisted.scripts.mktap import _tapHelper +from twisted.application.service import ServiceMaker -TwistedSSH = _tapHelper( +TwistedSSH = ServiceMaker( "Twisted Conch Server", "twisted.conch.tap", "A Conch SSH service.", "conch") -TwistedManhole = _tapHelper( +TwistedManhole = ServiceMaker( "Twisted Manhole (new)", "twisted.conch.manhole_tap", ("An interactive remote debugger service accessible via telnet "