def verifyHostKey(self, hostKey, fingerprint): host = self.transport.getPeer().host rv = self._isInKnownHosts(host, hostKey) if rv == 1: # valid key return defer.succeed(1) elif rv == 2: # key changed return defer.fail( ConchError("WARNING: %s' host key changed! Aborting!" % host)) elif rv == 3: return defer.fail( ConchError("Oppening ~/.ssh/known_hosts failed." % host)) else: return defer.fail( ConchError("WARNING: %s's host key unknown. Aborting!" % host))
def openShell(self, proto): from twisted.internet import reactor if not self.ptyTuple: # we didn't get a pty-req log.msg('tried to get shell without pty, failing') raise ConchError("no pty") uid, gid = self.avatar.getUserGroupId() homeDir = self.avatar.getHomeDir() shell = self.avatar.getShell() self.environ['USER'] = self.avatar.username self.environ['HOME'] = homeDir self.environ['SHELL'] = shell shellExec = os.path.basename(shell) peer = self.avatar.conn.transport.transport.getPeer() host = self.avatar.conn.transport.transport.getHost() self.environ['SSH_CLIENT'] = '%s %s %s' % (peer.host, peer.port, host.port) self.getPtyOwnership() self.pty = reactor.spawnProcess(proto, \ shell, ['-%s' % shellExec], self.environ, homeDir, uid, gid, usePTY = self.ptyTuple) self.addUTMPEntry() fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ, struct.pack('4H', *self.winSize)) if self.modes: self.setModes() self.oldWrite = proto.transport.write proto.transport.write = self._writeHack self.avatar.conn.transport.transport.setTcpNoDelay(1)
def testOpen(self): channel = SshClient.CommandChannel("foo") channel.openFailed(ConchError("quux", 22)) self.assertEqual( "None CommandChannel Open of foo failed (error code 22): quux", SshClient.log.message, )
def getPrivateKey(self): """ Try to load the private key from the last used file identified by C{getPublicKey}, potentially asking for the passphrase if the key is encrypted. """ file = os.path.expanduser(self.usedFiles[-1]) if not os.path.exists(file): return None try: return defer.succeed(keys.Key.fromFile(file)) except keys.EncryptedKeyError: for i in range(3): prompt = "Enter passphrase for key '%s': " % self.usedFiles[-1] try: p = self._getPassword(prompt).encode( sys.getfilesystemencoding()) return defer.succeed(keys.Key.fromFile(file, passphrase=p)) except (keys.BadKeyError, ConchError): pass return defer.fail(ConchError('bad password')) raise except KeyboardInterrupt: print() reactor.stop()
def openShell(self, proto): if not self.ptyTuple: # We didn't get a pty-req. self._log.error("tried to get shell without pty, failing") raise ConchError("no pty") uid, gid = self.avatar.getUserGroupId() homeDir = self.avatar.getHomeDir() shell = self.avatar.getShell() self.environ["USER"] = self.avatar.username self.environ["HOME"] = homeDir self.environ["SHELL"] = shell shellExec = os.path.basename(shell) peer = self.avatar.conn.transport.transport.getPeer() host = self.avatar.conn.transport.transport.getHost() self.environ["SSH_CLIENT"] = "%s %s %s" % (peer.host, peer.port, host.port) self.getPtyOwnership() self.pty = self._reactor.spawnProcess( proto, shell, ["-%s" % (shellExec, )], self.environ, homeDir, uid, gid, usePTY=self.ptyTuple, ) self.addUTMPEntry() fcntl.ioctl(self.pty.fileno(), tty.TIOCSWINSZ, struct.pack("4H", *self.winSize)) if self.modes: self.setModes() self.oldWrite = proto.transport.write proto.transport.write = self._writeHack self.avatar.conn.transport.transport.setTcpNoDelay(1)
def lookupChannel(self, channelType, windowSize, maxPacket, data): klass = self.channelLookup.get(channelType, None) if not klass: raise ConchError(OPEN_UNKNOWN_CHANNEL_TYPE, "unknown channel") else: return klass(remoteWindow=windowSize, remoteMaxPacket=maxPacket, data=data, avatar=self)
def msg_sendRequest(self, lst): cn, requestType, data, wantReply = lst if not self.haveChannel(cn): if wantReply: self.returnDeferredWire(defer.fail(ConchError("no channel"))) channel = self.getChannel(cn) d = self.conn.sendRequest(channel, requestType, data, wantReply) if wantReply: self.returnDeferredWire(d)
def connect(host, port, options, verifyHostKey, userAuthObject): if options['nocache']: return defer.fail(ConchError('not using connection caching')) d = defer.Deferred() filename = os.path.expanduser("~/.conch-%s-%s-%i" % (userAuthObject.user, host, port)) factory = SSHUnixClientFactory(d, options, userAuthObject) reactor.connectUNIX(filename, factory, timeout=2, checkPID=1) return d
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)
def _getPassword(self, prompt): try: oldout, oldin = sys.stdout, sys.stdin sys.stdin = sys.stdout = open('/dev/tty', 'r+') p = getpass.getpass(prompt) sys.stdout, sys.stdin = oldout, oldin return p except (KeyboardInterrupt, IOError): print raise ConchError('PEBKAC')
def channelClosed(self, channel): # Work around a bug in Conch 0.8 - channelClosed tries to close # channels which are not really open yet. This is fixed since twisted's # SVN revision 20700. # FIXME: Remove after upgrade to newer twisted. if channel in self.channelsToRemoteChannel: connection.SSHConnection.channelClosed(self, channel) else: # See http://twistedmatrix.com/trac/ticket/2782. channel.openFailed(ConchError("Service stopped"))
def _verify_host_key(transport, host, pubKey, fingerprint): expected = transport.factory.options.get("fingerprint", None) if fingerprint == expected: return succeed(1) log.error( "SSH Host Key fingerprint of ({fp}) does not match the expected value of ({expected}).", fp=fingerprint, expected=expected) return fail(ConchError("Host fingerprint is unexpected."))
def processEnded(self, reason): """ 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)
def lookupChannel(self, channelType, windowSize, maxPacket, data): """ Override this to get more info on the unknown channel """ klass = self.channelLookup.get(channelType, None) if not klass: raise ConchError(OPEN_UNKNOWN_CHANNEL_TYPE, f"unknown channel: {channelType}") else: return klass(remoteWindow=windowSize, remoteMaxPacket=maxPacket, data=data, avatar=self)
def _cbRequestIdentities(self, data): if ord(data[0]) != AGENT_IDENTITIES_ANSWER: return ConchError('unexpected respone: %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)) return keys
def channel_forwarded_tcpip(self, windowSize, maxPacket, data): log.msg('%s %s' % ('FTCP', repr(data))) remoteHP, origHP = forwarding.unpackOpen_forwarded_tcpip(data) log.msg(self.remoteForwards) log.msg(remoteHP) if remoteHP[1] in self.remoteForwards: connectHP = self.remoteForwards[remoteHP[1]] log.msg('connect forwarding %s' % (connectHP,)) return SSHConnectForwardingChannel(connectHP, remoteWindow = windowSize, remoteMaxPacket = maxPacket, conn = self) else: raise ConchError(connection.OPEN_CONNECT_FAILED, "don't know about that port")
def dataReceived(self, data): self.buf += data while 1: 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:] reqType = ord(packet[0]) d = self.deferreds.pop(0) if reqType == AGENT_FAILURE: d.errback(ConchError('agent failure')) elif reqType == AGENT_SUCCESS: d.callback('') else: d.callback(packet)
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: 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): blob, data = getNS(data) comment, data = getNS(data) keys.append((blob, comment)) return keys
def processEnded(self, reason): """ 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: {} ({})".format( reason.value.exitCode, self.problems.decode("charmap"), ))) else: buf = self.buf.replace(b"\r\n", b"\n") self._getDeferred().callback(buf)
def _getPassword(self, prompt): """ Prompt for a password using L{getpass.getpass}. @param prompt: Written on tty to ask for the input. @type prompt: L{str} @return: The input. @rtype: L{str} """ with self._replaceStdoutStdin(): try: p = getpass.getpass(prompt) return p except (KeyboardInterrupt, IOError): print() raise ConchError('PEBKAC')
def channel_forwarded_tcpip(self, windowSize, maxPacket, data): ''' This gets called when the remote forwared port gets a connection request ''' log.msg('%s %s' % ('FTCP', repr(data))) remoteHP, origHP = forwarding.unpackOpen_forwarded_tcpip(data) log.msg(self.remoteForwards) log.msg(remoteHP) if self.remoteForwards.has_key(remoteHP[1]): connectHP = self.remoteForwards[remoteHP[1]] log.msg('connect forwarding %s' % (connectHP, )) return SSHConnectForwardingChannel(connectHP, remoteWindow=windowSize, remoteMaxPacket=maxPacket, conn=self) else: raise ConchError(connection.OPEN_CONNECT_FAILED, "don't know about that port")
def getPrivateKey(self): file = os.path.expanduser(self.usedFiles[-1]) if not os.path.exists(file): return None try: return defer.succeed(keys.getPrivateKeyObject(file)) except keys.BadKeyError, e: if e.args[0] == 'encrypted key with no passphrase': for i in range(3): prompt = "Enter passphrase for key '%s': " % \ self.usedFiles[-1] try: p = self._getPassword(prompt) return defer.succeed( keys.getPrivateKeyObject(file, passphrase=p)) except (keys.BadKeyError, ConchError): pass return defer.fail(ConchError('bad password')) raise
def ssh_USERAUTH_REQUEST(self, packet): # This is copied and pasted from twisted/conch/ssh/userauth.py in # Twisted 8.0.1. We do this so we can add _ebLogToBanner between # two existing errbacks. user, nextService, method, rest = getNS(packet, 3) if user != self.user or nextService != self.nextService: self.authenticatedWith = [] # clear auth state self.user = user self.nextService = nextService self.method = method d = self.tryAuth(method, user, rest) if not d: self._ebBadAuth(failure.Failure(ConchError('auth returned none'))) return d.addCallback(self._sendConfiguredBanner) d.addCallbacks(self._cbFinishedAuth) d.addErrback(self._ebMaybeBadAuth) # This line does not appear in the original. d.addErrback(self._ebLogToBanner) d.addErrback(self._ebBadAuth) return d
def _cbSignData(self, data): if ord(data[0]) != AGENT_SIGN_RESPONSE: raise ConchError('unexpected data: %i' % ord(data[0])) signature = getNS(data[1:])[0] return signature
def auth(self, auth_service, argv): """Verify we have permission to run the request command.""" # Key fingerprint if hasattr(self.user.meta, "fingerprint"): fingerprint = self.user.meta.fingerprint else: fingerprint = None if hasattr(self.user.meta, "password"): password = self.user.meta.password else: password = None # Check permissions by mapping requested path to file system path repostring = argv[-1] repolist = repostring.split('/') if repolist[0]: # No leading / scheme = repolist[0] projectpath = repolist[1:] else: scheme = repolist[1] projectpath = repolist[2:] repopath = self.user.meta.repopath(scheme, projectpath) if not repopath: return Failure( ConchError( "The remote repository at '{0}' does not exist. Verify that your remote is correct." .format(repostring))) projectname = self.user.meta.projectname(repostring) # Map the user users = auth_service["users"] user = self.map_user(self.user.username, fingerprint, users) execGitCommand = repopath, user, auth_service # Check to see if anonymous read access is enabled and if # this is a read if (not self.user.meta.anonymousReadAccess or \ 'git-upload-pack' not in argv[:-1]): # First, error out if the project itself is disabled. if not auth_service["status"]: error = "Project {1} has been disabled.".format(projectname) return Failure(ConchError(error)) # If anonymous access for this type of command is not allowed, # check if the user is a maintainer on this project # global values - d.o issue #1036686 # "git":key if self.user.username == "git" and user and not user["global"]: return execGitCommand # Username in maintainers list elif self.user.username not in users: error = "User '{1}' does not have write permissions for repository '{2}'".format( projectname, self.user.username) return Failure(ConchError(error)) elif not user["global"]: # username:key if fingerprint in user["ssh_keys"].values(): return execGitCommand # username:password elif user["pass"] == password: return execGitCommand else: # Both kinds of username auth failed error = "Permission denied when accessing '{1}' as user '{2}'".format( projectname, self.user.username) return Failure(ConchError(error)) else: # Account is globally disabled or disallowed # 0x01 = no Git user role, but unknown reason (probably a bug!) # 0x02 = Git account suspended # 0x04 = Git ToS unchecked # 0x08 = Drupal.org account blocked error = '' if user["global"] & 0x02: error += "Your Git access has been suspended.\n" if user["global"] & 0x04: error += "You are required to accept the Git Access Agreement in your user profile before using Git.\n" if user["global"] & 0x08: error += "Your Drupal.org account has been blocked.\n" if not error and user["global"] == 0x01: error = "You do not have permission to access '{0}' with the provided credentials.\n".format( projectname) elif not error: # unknown situation, but be safe and error out error = "This operation cannot be completed at this time. It may be that we are experiencing technical difficulties or are currently undergoing maintenance." return Failure(ConchError(error)) else: # Read only command and anonymous access is enabled return execGitCommand
def _cbSignData(self, data): if data[0] != chr(AGENT_SIGN_RESPONSE): return ConchError('unexpected data: %i' % ord(data[0])) signature = getNS(data[1:])[0] return signature
def testOpen(self): channel = SshClient.CommandChannel('foo') channel.openFailed(ConchError('quux', 22)) self.assertEqual( 'None CommandChannel Open of foo failed (error code 22): quux', SshClient.log.message)
def auth(self, auth_service, argv): """Verify we have permission to run the request command.""" # Key fingerprint if hasattr(self.user.meta, "fingerprint"): fingerprint = self.user.meta.fingerprint else: fingerprint = None if hasattr(self.user.meta, "password"): password = self.user.meta.password else: password = None # Check permissions by mapping requested path to file system path repostring = argv[-1] repolist = repostring.split('/') scheme = repolist[1] projectpath = repolist[2:] repopath = self.user.meta.repopath(scheme, projectpath) if not repopath: return Failure(ConchError("The remote repository at '{0}' does not exist. Verify that your remote is correct.".format(repostring))) # Map the user users = auth_service["users"] user = self.map_user(self.user.username, fingerprint, users) # Check to see if anonymous read access is enabled and if # this is a read if (not self.user.meta.anonymousReadAccess or \ 'git-upload-pack' not in argv[:-1]): # If anonymous access for this type of command is not allowed, # check if the user is a maintainer on this project # global values - d.o issue #1036686 # "git":key if self.user.username == "git" and user and not user["global"]: return repopath, user, auth_service["repo_id"] # Username in maintainers list elif self.user.username in users and not user["global"]: # username:key if fingerprint in user["ssh_keys"].values(): return repopath, user, auth_service["repo_id"] # username:password elif user["pass"] == password: return repopath, user, auth_service["repo_id"] else: # Both kinds of username auth failed error = "Permission denied when accessing '{1}' as user '{2}'".format(argv[-1], self.user.username) return Failure(ConchError(error)) else: # Account is globally disabled or disallowed # 0 = ok, 1 = suspended, 2 = ToS unchecked, 3 = other reason if user and user["global"] == 1: error = "Your account is suspended." elif user and user["global"] == 2: error = "You are required to accept the Git Access Agreement in your user profile before using git." elif user and user["global"] == 3: error = "This operation cannot be completed at this time. It may be that we are experiencing technical difficulties or are currently undergoing maintenance." else: error = "You do not have permission to access '{0}' with the provided credentials.".format(argv[-1]) return Failure(ConchError(error)) else: # Read only command and anonymous access is enabled return repopath, user, auth_service["repo_id"]
def getChannel(self, channelID): channel = self.conn.channels[channelID] if not isinstance(channel, SSHUnixChannel): raise ConchError('nice try bub') return channel