def test_mismatchedHostKey(self): """ If the SSH public key presented by the SSH server does not match the previously remembered key, as reported by the L{KnownHostsFile} instance use to construct the endpoint, for that server, the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping L{HostKeyChanged}. """ differentKey = Key.fromString(privateDSA_openssh).public() knownHosts = KnownHostsFile(self.mktemp()) knownHosts.addHostKey(self.serverAddress.host, differentKey) knownHosts.addHostKey(self.hostname, differentKey) # The UI may answer true to any questions asked of it; they should # make no difference, since a *mismatched* key is not even optionally # allowed to complete a connection. ui = FixedResponseUI(True) endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, password=b"dummy password", knownHosts=knownHosts, ui=ui) factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) f = self.failureResultOf(connected) f.trap(HostKeyChanged)
def test_iterentriesUnsaved(self): """ If the save path for a L{KnownHostsFile} does not exist, L{KnownHostsFile.iterentries} still returns added but unsaved entries. """ hostsFile = KnownHostsFile(FilePath(self.mktemp())) hostsFile.addHostKey("www.example.com", Key.fromString(sampleKey)) self.assertEqual(1, len(list(hostsFile.iterentries())))
def test_saveResetsClobberState(self): """ After L{KnownHostsFile.save} is used once with an instance initialized by the default initializer, contents of the save path are respected and preserved. """ hostsFile = KnownHostsFile(self.pathWithContent(sampleHashedLine)) preSave = hostsFile.addHostKey("www.example.com", Key.fromString(otherSampleKey)) hostsFile.save() postSave = hostsFile.addHostKey("another.example.com", Key.fromString(thirdSampleKey)) hostsFile.save() self.assertEqual([preSave, postSave], list(hostsFile.iterentries()))
def test_saveResetsClobberState(self): """ After L{KnownHostsFile.save} is used once with an instance initialized by the default initializer, contents of the save path are respected and preserved. """ hostsFile = KnownHostsFile(self.pathWithContent(sampleHashedLine)) preSave = hostsFile.addHostKey( "www.example.com", Key.fromString(otherSampleKey)) hostsFile.save() postSave = hostsFile.addHostKey( "another.example.com", Key.fromString(thirdSampleKey)) hostsFile.save() self.assertEqual([preSave, postSave], list(hostsFile.iterentries()))
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 setUp(self): """ Configure an SSH server with password authentication enabled for a well-known (to the tests) account. """ SSHCommandClientEndpointTestsMixin.setUp(self) knownHosts = KnownHostsFile(FilePath(self.mktemp())) knownHosts.addHostKey( self.hostname, self.factory.publicKeys['ssh-rsa']) knownHosts.addHostKey( self.serverAddress.host, self.factory.publicKeys['ssh-rsa']) self.endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, password=self.password, knownHosts=knownHosts, ui=FixedResponseUI(False))
def test_readExisting(self): """ Existing entries in the I{known_hosts} file are reflected by the L{KnownHostsFile} created by L{_NewConnectionHelper} when none is supplied to it. """ key = CommandFactory().publicKeys['ssh-rsa'] path = FilePath(self.mktemp()) knownHosts = KnownHostsFile(path) knownHosts.addHostKey("127.0.0.1", key) knownHosts.save() msg("Created known_hosts file at %r" % (path.path,)) # Unexpand ${HOME} to make sure ~ syntax is respected. home = os.path.expanduser("~/") default = path.path.replace(home, "~/") self.patch(_NewConnectionHelper, "_KNOWN_HOSTS", default) msg("Patched _KNOWN_HOSTS with %r" % (default,)) loaded = _NewConnectionHelper._knownHosts() self.assertTrue(loaded.hasHostKey("127.0.0.1", key))
def test_readExisting(self): """ Existing entries in the I{known_hosts} file are reflected by the L{KnownHostsFile} created by L{_NewConnectionHelper} when none is supplied to it. """ key = CommandFactory().publicKeys['ssh-rsa'] path = FilePath(self.mktemp()) knownHosts = KnownHostsFile(path) knownHosts.addHostKey("127.0.0.1", key) knownHosts.save() msg("Created known_hosts file at %r" % (path.path, )) # Unexpand ${HOME} to make sure ~ syntax is respected. home = os.path.expanduser("~/") default = path.path.replace(home, "~/") self.patch(_NewConnectionHelper, "_KNOWN_HOSTS", default) msg("Patched _KNOWN_HOSTS with %r" % (default, )) loaded = _NewConnectionHelper._knownHosts() self.assertTrue(loaded.hasHostKey("127.0.0.1", key))
def setUp(self): """ Configure an SSH server with password authentication enabled for a well-known (to the tests) account. """ SSHCommandClientEndpointTestsMixin.setUp(self) knownHosts = KnownHostsFile(FilePath(self.mktemp())) knownHosts.addHostKey(self.hostname, self.factory.publicKeys['ssh-rsa']) knownHosts.addHostKey(self.serverAddress.host, self.factory.publicKeys['ssh-rsa']) self.endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, password=self.password, knownHosts=knownHosts, ui=FixedResponseUI(False))
def test_savingAvoidsDuplication(self): """ L{KnownHostsFile.save} only writes new entries to the save path, not entries which were added and already written by a previous call to C{save}. """ path = FilePath(self.mktemp()) knownHosts = KnownHostsFile(path) entry = knownHosts.addHostKey("some.example.com", Key.fromString(sampleKey)) knownHosts.save() knownHosts.save() knownHosts = KnownHostsFile.fromPath(path) self.assertEqual([entry], list(knownHosts.iterentries()))
def test_defaultInitializerClobbersExisting(self): """ After using the default initializer for L{KnownHostsFile}, the first use of L{KnownHostsFile.save} overwrites any existing contents in the save path. """ path = self.pathWithContent(sampleHashedLine) hostsFile = KnownHostsFile(path) entry = hostsFile.addHostKey("www.example.com", Key.fromString(otherSampleKey)) hostsFile.save() # Check KnownHostsFile to see what it thinks the state is self.assertEqual([entry], list(hostsFile.iterentries())) # And also directly check the underlying file itself self.assertEqual(entry.toString() + "\n", path.getContent())
def test_savingAvoidsDuplication(self): """ L{KnownHostsFile.save} only writes new entries to the save path, not entries which were added and already written by a previous call to C{save}. """ path = FilePath(self.mktemp()) knownHosts = KnownHostsFile(path) entry = knownHosts.addHostKey( "some.example.com", Key.fromString(sampleKey)) knownHosts.save() knownHosts.save() knownHosts = KnownHostsFile.fromPath(path) self.assertEqual([entry], list(knownHosts.iterentries()))
def test_defaultInitializerClobbersExisting(self): """ After using the default initializer for L{KnownHostsFile}, the first use of L{KnownHostsFile.save} overwrites any existing contents in the save path. """ path = self.pathWithContent(sampleHashedLine) hostsFile = KnownHostsFile(path) entry = hostsFile.addHostKey( "www.example.com", Key.fromString(otherSampleKey)) hostsFile.save() # Check KnownHostsFile to see what it thinks the state is self.assertEqual([entry], list(hostsFile.iterentries())) # And also directly check the underlying file itself self.assertEqual(entry.toString() + "\n", path.getContent())
def test_unsavedEntryHasKeyMismatch(self): """ L{KnownHostsFile.hasHostKey} raises L{HostKeyChanged} if the host key is present in memory (but not yet saved), but different from the expected one. The resulting exception has a C{offendingEntry} indicating the given entry, but no filename or line number information (reflecting the fact that the entry exists only in memory). """ hostsFile = KnownHostsFile(FilePath(self.mktemp())) entry = hostsFile.addHostKey("www.example.com", Key.fromString(otherSampleKey)) exception = self.assertRaises(HostKeyChanged, hostsFile.hasHostKey, "www.example.com", Key.fromString(thirdSampleKey)) self.assertEqual(exception.offendingEntry, entry) self.assertEqual(exception.lineno, None) self.assertEqual(exception.path, None)
def test_unsavedEntryHasKeyMismatch(self): """ L{KnownHostsFile.hasHostKey} raises L{HostKeyChanged} if the host key is present in memory (but not yet saved), but different from the expected one. The resulting exception has a C{offendingEntry} indicating the given entry, but no filename or line number information (reflecting the fact that the entry exists only in memory). """ hostsFile = KnownHostsFile(FilePath(self.mktemp())) entry = hostsFile.addHostKey( "www.example.com", Key.fromString(otherSampleKey)) exception = self.assertRaises( HostKeyChanged, hostsFile.hasHostKey, "www.example.com", Key.fromString(thirdSampleKey)) self.assertEqual(exception.offendingEntry, entry) self.assertEqual(exception.lineno, None) self.assertEqual(exception.path, None)
def setUp(self): """ Patch 'open' in verifyHostKey. """ self.fakeFile = FakeFile() self.patch(default, "_open", self.patchedOpen) self.hostsOption = self.mktemp() self.hashedEntries = {} knownHostsFile = KnownHostsFile(FilePath(self.hostsOption)) for host in (b"exists.example.com", b"4.3.2.1"): entry = knownHostsFile.addHostKey(host, Key.fromString(sampleKey)) self.hashedEntries[host] = entry knownHostsFile.save() self.fakeTransport = FakeObject() self.fakeTransport.factory = FakeObject() self.options = self.fakeTransport.factory.options = { 'host': b"exists.example.com", 'known-hosts': self.hostsOption }
class NewConnectionTests(TestCase, SSHCommandClientEndpointTestsMixin): """ Tests for L{SSHCommandClientEndpoint} when using the C{newConnection} constructor. """ def setUp(self): """ Configure an SSH server with password authentication enabled for a well-known (to the tests) account. """ SSHCommandClientEndpointTestsMixin.setUp(self) # Make the server's host key available to be verified by the client. self.hostKeyPath = FilePath(self.mktemp()) self.knownHosts = KnownHostsFile(self.hostKeyPath) self.knownHosts.addHostKey( self.hostname, self.factory.publicKeys['ssh-rsa']) self.knownHosts.addHostKey( self.serverAddress.host, self.factory.publicKeys['ssh-rsa']) self.knownHosts.save() def create(self): """ Create and return a new L{SSHCommandClientEndpoint} using the C{newConnection} constructor. """ return SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, password=self.password, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) def finishConnection(self): """ Establish the first attempted TCP connection using the SSH server which C{self.factory} can create. """ return self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) def assertClientTransportState(self, client, immediateClose): """ Assert that the transport for the given protocol has been disconnected. L{SSHCommandClientEndpoint.newConnection} creates a new dedicated SSH connection and cleans it up after the command exits. """ # Nothing useful can be done with the connection at this point, so the # endpoint should close it. if immediateClose: self.assertTrue(client.transport.aborted) else: self.assertTrue(client.transport.disconnecting) def test_destination(self): """ L{SSHCommandClientEndpoint} uses the L{IReactorTCP} passed to it to attempt a connection to the host/port address also passed to it. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, password=self.password, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol endpoint.connect(factory) host, port, factory, timeout, bindAddress = self.reactor.tcpClients[0] self.assertEqual(self.hostname, host) self.assertEqual(self.port, port) self.assertEqual(1, len(self.reactor.tcpClients)) def test_connectionFailed(self): """ If a connection cannot be established, the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} representing the reason for the connection setup failure. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol d = endpoint.connect(factory) factory = self.reactor.tcpClients[0][2] factory.clientConnectionFailed(None, Failure(ConnectionRefusedError())) self.failureResultOf(d).trap(ConnectionRefusedError) def test_userRejectedHostKey(self): """ If the L{KnownHostsFile} instance used to construct L{SSHCommandClientEndpoint} rejects the SSH public key presented by the server, the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping L{UserRejectedKey}. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, knownHosts=KnownHostsFile(self.mktemp()), ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) f = self.failureResultOf(connected) f.trap(UserRejectedKey) def test_mismatchedHostKey(self): """ If the SSH public key presented by the SSH server does not match the previously remembered key, as reported by the L{KnownHostsFile} instance use to construct the endpoint, for that server, the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping L{HostKeyChanged}. """ differentKey = Key.fromString(privateDSA_openssh).public() knownHosts = KnownHostsFile(self.mktemp()) knownHosts.addHostKey(self.serverAddress.host, differentKey) knownHosts.addHostKey(self.hostname, differentKey) # The UI may answer true to any questions asked of it; they should # make no difference, since a *mismatched* key is not even optionally # allowed to complete a connection. ui = FixedResponseUI(True) endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, password=b"dummy password", knownHosts=knownHosts, ui=ui) factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) f = self.failureResultOf(connected) f.trap(HostKeyChanged) def test_connectionClosedBeforeSecure(self): """ If the connection closes at any point before the SSH transport layer has finished key exchange (ie, gotten to the point where we may attempt to authenticate), the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping the reason for the lost connection. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol d = endpoint.connect(factory) transport = StringTransport() factory = self.reactor.tcpClients[0][2] client = factory.buildProtocol(None) client.makeConnection(transport) client.connectionLost(Failure(ConnectionDone())) self.failureResultOf(d).trap(ConnectionDone) def test_connectionCancelledBeforeSecure(self): """ If the connection is cancelled before the SSH transport layer has finished key exchange (ie, gotten to the point where we may attempt to authenticate), the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping L{CancelledError} and the connection is aborted. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol d = endpoint.connect(factory) transport = AbortableFakeTransport(None, isServer=False) factory = self.reactor.tcpClients[0][2] client = factory.buildProtocol(None) client.makeConnection(transport) d.cancel() self.failureResultOf(d).trap(CancelledError) self.assertTrue(transport.aborted) # Make sure the connection closing doesn't result in unexpected # behavior when due to cancellation: client.connectionLost(Failure(ConnectionDone())) def test_connectionCancelledBeforeConnected(self): """ If the connection is cancelled before it finishes connecting, the connection attempt is stopped. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol d = endpoint.connect(factory) d.cancel() self.failureResultOf(d).trap(ConnectingCancelledError) self.assertTrue(self.reactor.connectors[0].stoppedConnecting) def test_passwordAuthenticationFailure(self): """ If the SSH server rejects the password presented during authentication, the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping L{AuthenticationFailed}. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, password=b"dummy password", knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) # For security, the server delays password authentication failure # response. Advance the simulation clock so the client sees the # failure. self.reactor.advance(server.service.passwordDelay) # Let the failure response traverse the "network" pump.flush() f = self.failureResultOf(connected) f.trap(AuthenticationFailed) # XXX Should assert something specific about the arguments of the # exception self.assertClientTransportState(client, False) def setupKeyChecker(self, portal, users): """ Create an L{ISSHPrivateKey} checker which recognizes C{users} and add it to C{portal}. @param portal: A L{Portal} to which to add the checker. @type portal: L{Portal} @param users: The users and their keys the checker will recognize. Keys are byte strings giving user names. Values are byte strings giving OpenSSH-formatted private keys. @type users: C{dict} """ credentials = {} for username, keyString in users.iteritems(): goodKey = Key.fromString(keyString) authorizedKeys = FilePath(self.mktemp()) authorizedKeys.setContent(goodKey.public().toString("OPENSSH")) credentials[username] = [authorizedKeys] checker = MemorySSHPublicKeyDatabase(credentials) portal.registerChecker(checker) def test_publicKeyAuthenticationFailure(self): """ If the SSH server rejects the key pair presented during authentication, the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping L{AuthenticationFailed}. """ badKey = Key.fromString(privateRSA_openssh) self.setupKeyChecker(self.portal, {self.user: privateDSA_openssh}) endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, keys=[badKey], knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) f = self.failureResultOf(connected) f.trap(AuthenticationFailed) # XXX Should assert something specific about the arguments of the # exception # Nothing useful can be done with the connection at this point, so the # endpoint should close it. self.assertTrue(client.transport.disconnecting) def test_authenticationFallback(self): """ If the SSH server does not accept any of the specified SSH keys, the specified password is tried. """ badKey = Key.fromString(privateRSA_openssh) self.setupKeyChecker(self.portal, {self.user: privateDSA_openssh}) endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, keys=[badKey], password=self.password, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) # Exercising fallback requires a failed authentication attempt. Allow # one. self.factory.attemptsBeforeDisconnect += 1 server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) pump.pump() # The server logs the channel open failure - this is expected. errors = self.flushLoggedErrors(ConchError) self.assertIn( 'unknown channel', (errors[0].value.data, errors[0].value.value)) self.assertEqual(1, len(errors)) # Now deal with the results on the endpoint side. f = self.failureResultOf(connected) f.trap(ConchError) self.assertEqual('unknown channel', f.value.value) # Nothing useful can be done with the connection at this point, so the # endpoint should close it. self.assertTrue(client.transport.disconnecting) def test_publicKeyAuthentication(self): """ If L{SSHCommandClientEndpoint} is initialized with any private keys, it will try to use them to authenticate with the SSH server. """ key = Key.fromString(privateDSA_openssh) self.setupKeyChecker(self.portal, {self.user: privateDSA_openssh}) self.realm.channelLookup[b'session'] = WorkingExecSession endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, keys=[key], knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) protocol = self.successResultOf(connected) self.assertNotIdentical(None, protocol.transport) def test_agentAuthentication(self): """ If L{SSHCommandClientEndpoint} is initialized with an L{SSHAgentClient}, the agent is used to authenticate with the SSH server. """ key = Key.fromString(privateRSA_openssh) agentServer = SSHAgentServer() agentServer.factory = Factory() agentServer.factory.keys = {key.blob(): (key, "")} self.setupKeyChecker(self.portal, {self.user: privateRSA_openssh}) agentEndpoint = SingleUseMemoryEndpoint(agentServer) endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, knownHosts=self.knownHosts, ui=FixedResponseUI(False), agentEndpoint=agentEndpoint) self.realm.channelLookup[b'session'] = WorkingExecSession factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) # Let the agent client talk with the agent server and the ssh client # talk with the ssh server. for i in range(14): agentEndpoint.pump.pump() pump.pump() protocol = self.successResultOf(connected) self.assertNotIdentical(None, protocol.transport) def test_loseConnection(self): """ The transport connected to the protocol has a C{loseConnection} method which causes the channel in which the command is running to close and the overall connection to be closed. """ self.realm.channelLookup[b'session'] = WorkingExecSession endpoint = self.create() factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.finishConnection() protocol = self.successResultOf(connected) closed = self.record(server, protocol, 'closed', noArgs=True) protocol.transport.loseConnection() pump.pump() self.assertEqual([None], closed) # Let the last bit of network traffic flow. This lets the server's # close acknowledgement through, at which point the client can close # the overall SSH connection. pump.pump() # Nothing useful can be done with the connection at this point, so the # endpoint should close it. self.assertTrue(client.transport.disconnecting)
class NewConnectionTests(TestCase, SSHCommandClientEndpointTestsMixin): """ Tests for L{SSHCommandClientEndpoint} when using the C{newConnection} constructor. """ def setUp(self): """ Configure an SSH server with password authentication enabled for a well-known (to the tests) account. """ SSHCommandClientEndpointTestsMixin.setUp(self) # Make the server's host key available to be verified by the client. self.hostKeyPath = FilePath(self.mktemp()) self.knownHosts = KnownHostsFile(self.hostKeyPath) self.knownHosts.addHostKey(self.hostname, self.factory.publicKeys['ssh-rsa']) self.knownHosts.addHostKey(self.serverAddress.host, self.factory.publicKeys['ssh-rsa']) self.knownHosts.save() def create(self): """ Create and return a new L{SSHCommandClientEndpoint} using the C{newConnection} constructor. """ return SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, password=self.password, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) def finishConnection(self): """ Establish the first attempted TCP connection using the SSH server which C{self.factory} can create. """ return self.connectedServerAndClient(self.factory, self.reactor.tcpClients[0][2]) def assertClientTransportState(self, client, immediateClose): """ Assert that the transport for the given protocol has been disconnected. L{SSHCommandClientEndpoint.newConnection} creates a new dedicated SSH connection and cleans it up after the command exits. """ # Nothing useful can be done with the connection at this point, so the # endpoint should close it. if immediateClose: self.assertTrue(client.transport.aborted) else: self.assertTrue(client.transport.disconnecting) def test_destination(self): """ L{SSHCommandClientEndpoint} uses the L{IReactorTCP} passed to it to attempt a connection to the host/port address also passed to it. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, password=self.password, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol endpoint.connect(factory) host, port, factory, timeout, bindAddress = self.reactor.tcpClients[0] self.assertEqual(self.hostname, host) self.assertEqual(self.port, port) self.assertEqual(1, len(self.reactor.tcpClients)) def test_connectionFailed(self): """ If a connection cannot be established, the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} representing the reason for the connection setup failure. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol d = endpoint.connect(factory) factory = self.reactor.tcpClients[0][2] factory.clientConnectionFailed(None, Failure(ConnectionRefusedError())) self.failureResultOf(d).trap(ConnectionRefusedError) def test_userRejectedHostKey(self): """ If the L{KnownHostsFile} instance used to construct L{SSHCommandClientEndpoint} rejects the SSH public key presented by the server, the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping L{UserRejectedKey}. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, knownHosts=KnownHostsFile(self.mktemp()), ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) f = self.failureResultOf(connected) f.trap(UserRejectedKey) def test_mismatchedHostKey(self): """ If the SSH public key presented by the SSH server does not match the previously remembered key, as reported by the L{KnownHostsFile} instance use to construct the endpoint, for that server, the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping L{HostKeyChanged}. """ differentKey = Key.fromString(privateDSA_openssh).public() knownHosts = KnownHostsFile(self.mktemp()) knownHosts.addHostKey(self.serverAddress.host, differentKey) knownHosts.addHostKey(self.hostname, differentKey) # The UI may answer true to any questions asked of it; they should # make no difference, since a *mismatched* key is not even optionally # allowed to complete a connection. ui = FixedResponseUI(True) endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, password=b"dummy password", knownHosts=knownHosts, ui=ui) factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) f = self.failureResultOf(connected) f.trap(HostKeyChanged) def test_connectionClosedBeforeSecure(self): """ If the connection closes at any point before the SSH transport layer has finished key exchange (ie, gotten to the point where we may attempt to authenticate), the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping the reason for the lost connection. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol d = endpoint.connect(factory) transport = StringTransport() factory = self.reactor.tcpClients[0][2] client = factory.buildProtocol(None) client.makeConnection(transport) client.connectionLost(Failure(ConnectionDone())) self.failureResultOf(d).trap(ConnectionDone) def test_connectionCancelledBeforeSecure(self): """ If the connection is cancelled before the SSH transport layer has finished key exchange (ie, gotten to the point where we may attempt to authenticate), the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping L{CancelledError} and the connection is aborted. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol d = endpoint.connect(factory) transport = AbortableFakeTransport(None, isServer=False) factory = self.reactor.tcpClients[0][2] client = factory.buildProtocol(None) client.makeConnection(transport) d.cancel() self.failureResultOf(d).trap(CancelledError) self.assertTrue(transport.aborted) # Make sure the connection closing doesn't result in unexpected # behavior when due to cancellation: client.connectionLost(Failure(ConnectionDone())) def test_connectionCancelledBeforeConnected(self): """ If the connection is cancelled before it finishes connecting, the connection attempt is stopped. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol d = endpoint.connect(factory) d.cancel() self.failureResultOf(d).trap(ConnectingCancelledError) self.assertTrue(self.reactor.connectors[0].stoppedConnecting) def test_passwordAuthenticationFailure(self): """ If the SSH server rejects the password presented during authentication, the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping L{AuthenticationFailed}. """ endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", b"dummy user", self.hostname, self.port, password=b"dummy password", knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) # For security, the server delays password authentication failure # response. Advance the simulation clock so the client sees the # failure. self.reactor.advance(server.service.passwordDelay) # Let the failure response traverse the "network" pump.flush() f = self.failureResultOf(connected) f.trap(AuthenticationFailed) # XXX Should assert something specific about the arguments of the # exception self.assertClientTransportState(client, False) def setupKeyChecker(self, portal, users): """ Create an L{ISSHPrivateKey} checker which recognizes C{users} and add it to C{portal}. @param portal: A L{Portal} to which to add the checker. @type portal: L{Portal} @param users: The users and their keys the checker will recognize. Keys are byte strings giving user names. Values are byte strings giving OpenSSH-formatted private keys. @type users: C{dict} """ credentials = {} for username, keyString in users.iteritems(): goodKey = Key.fromString(keyString) authorizedKeys = FilePath(self.mktemp()) authorizedKeys.setContent(goodKey.public().toString("OPENSSH")) credentials[username] = [authorizedKeys] checker = MemorySSHPublicKeyDatabase(credentials) portal.registerChecker(checker) def test_publicKeyAuthenticationFailure(self): """ If the SSH server rejects the key pair presented during authentication, the L{Deferred} returned by L{SSHCommandClientEndpoint.connect} fires with a L{Failure} wrapping L{AuthenticationFailed}. """ badKey = Key.fromString(privateRSA_openssh) self.setupKeyChecker(self.portal, {self.user: privateDSA_openssh}) endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, keys=[badKey], knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) f = self.failureResultOf(connected) f.trap(AuthenticationFailed) # XXX Should assert something specific about the arguments of the # exception # Nothing useful can be done with the connection at this point, so the # endpoint should close it. self.assertTrue(client.transport.disconnecting) def test_authenticationFallback(self): """ If the SSH server does not accept any of the specified SSH keys, the specified password is tried. """ badKey = Key.fromString(privateRSA_openssh) self.setupKeyChecker(self.portal, {self.user: privateDSA_openssh}) endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, keys=[badKey], password=self.password, knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) # Exercising fallback requires a failed authentication attempt. Allow # one. self.factory.attemptsBeforeDisconnect += 1 server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) pump.pump() # The server logs the channel open failure - this is expected. errors = self.flushLoggedErrors(ConchError) self.assertIn('unknown channel', (errors[0].value.data, errors[0].value.value)) self.assertEqual(1, len(errors)) # Now deal with the results on the endpoint side. f = self.failureResultOf(connected) f.trap(ConchError) self.assertEqual('unknown channel', f.value.value) # Nothing useful can be done with the connection at this point, so the # endpoint should close it. self.assertTrue(client.transport.disconnecting) def test_publicKeyAuthentication(self): """ If L{SSHCommandClientEndpoint} is initialized with any private keys, it will try to use them to authenticate with the SSH server. """ key = Key.fromString(privateDSA_openssh) self.setupKeyChecker(self.portal, {self.user: privateDSA_openssh}) self.realm.channelLookup[b'session'] = WorkingExecSession endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, keys=[key], knownHosts=self.knownHosts, ui=FixedResponseUI(False)) factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) protocol = self.successResultOf(connected) self.assertNotIdentical(None, protocol.transport) def test_agentAuthentication(self): """ If L{SSHCommandClientEndpoint} is initialized with an L{SSHAgentClient}, the agent is used to authenticate with the SSH server. """ key = Key.fromString(privateRSA_openssh) agentServer = SSHAgentServer() agentServer.factory = Factory() agentServer.factory.keys = {key.blob(): (key, "")} self.setupKeyChecker(self.portal, {self.user: privateRSA_openssh}) agentEndpoint = SingleUseMemoryEndpoint(agentServer) endpoint = SSHCommandClientEndpoint.newConnection( self.reactor, b"/bin/ls -l", self.user, self.hostname, self.port, knownHosts=self.knownHosts, ui=FixedResponseUI(False), agentEndpoint=agentEndpoint) self.realm.channelLookup[b'session'] = WorkingExecSession factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.connectedServerAndClient( self.factory, self.reactor.tcpClients[0][2]) # Let the agent client talk with the agent server and the ssh client # talk with the ssh server. for i in range(14): agentEndpoint.pump.pump() pump.pump() protocol = self.successResultOf(connected) self.assertNotIdentical(None, protocol.transport) def test_loseConnection(self): """ The transport connected to the protocol has a C{loseConnection} method which causes the channel in which the command is running to close and the overall connection to be closed. """ self.realm.channelLookup[b'session'] = WorkingExecSession endpoint = self.create() factory = Factory() factory.protocol = Protocol connected = endpoint.connect(factory) server, client, pump = self.finishConnection() protocol = self.successResultOf(connected) closed = self.record(server, protocol, 'closed', noArgs=True) protocol.transport.loseConnection() pump.pump() self.assertEqual([None], closed) # Let the last bit of network traffic flow. This lets the server's # close acknowledgement through, at which point the client can close # the overall SSH connection. pump.pump() # Nothing useful can be done with the connection at this point, so the # endpoint should close it. self.assertTrue(client.transport.disconnecting)