###
# Copyright (c) 2004, Ali Afshar (aafshar@gmail.com)
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright notice,
#     this list of conditions, and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions, and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#   * Neither the name of the author of this software nor the name of
#     contributors to this software may be used to endorse or promote products
#     derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###

"""
This module proved the Sshd plugin; it allows you to log in remotely to
your Supybot via SSH.
"""

#TODO: command to fetch the public key
#Keys in a file
#See herald.py how to do data files

### Thanks
#
# Many thanks to the Supybot people for an incredible product.
# Many thanks to the Twisted people for an incredible product.
# (in alphabetical order)
#
### References
#
# Supybot plugin authoring tutorial
# http://supybot.sourceforge.net/docs/PLUGIN-EXAMPLE
#
# Twisted SSh example
# http://twistedmatrix.com/documents/current/examples/sshsimpleserver.py
#
# Twisted credentials tutorial
# http://twistedmatrix.com/documents/current/howto/cred
#
# How to add SSH to python applications HowTo
# http://aa.homelinux.com/pjs/twisted/
###

import supybot

__revision__ = "$Id: Sshd.py,v 1.27 2005/01/16 20:03:00 ali Exp $"
__author__ = supybot.Author('Ali Afshar', 'ali', 'aafshar@gmail.com')

# Supybot imports
import supybot.log as log
import supybot.conf as conf
import supybot.ircdb as ircdb
import supybot.utils as utils
import supybot.world as world
import supybot.plugins as plugins
import supybot.ircmsgs as ircmsgs
import supybot.commands as commands
import supybot.privmsgs as privmsgs
import supybot.registry as registry
import supybot.callbacks as callbacks

# Twisted Imports
# NOTE: Have you got the bug-fixed twisted.conch.ssh.factory?
from twisted.python import components
from twisted.python import log as tlog
from twisted.conch import error, avatar
from twisted.protocols.basic import LineReceiver
from twisted.internet import reactor, protocol, defer
from twisted.cred import portal, checkers, credentials
from twisted.conch.checkers import SSHPublicKeyDatabase
from twisted.conch.ssh import factory, userauth, connection, keys, session, common

#f = open('/home/ali/log','w')
#tlog.startLogging(f)

# General Imports
import os
import md5
import threading
from Crypto.PublicKey import RSA

# erm, will work this out later, I guess we could ask the user what port etc
# etc
def configure(advanced):
    # This will be called by setup.py to configure this module.  Advanced is
    # a bool that specifies whether the user identified himself as an advanced
    # user or not.  You should effect your configuration by manipulating the
    # registry as appropriate.
    from questions import expect, anything, something, yn
    conf.registerPlugin('Sshd', True)

# This is derived from the registry Integer class
# As the config variables are set, the setValue checks the validity
class Port(registry.Integer):
    """Value must be an integer greater than 1024 and no greater than 65535."""
    def setValue(self, v):
        if v < 1024 or v > 65535:
            self.error()
        # Commented out for the damn error.
        super(Port, self).setValue(v)
        #registry.Integer.setValue(self, v)

conf.registerPlugin('Sshd')

# Of type Port, specified above
conf.registerGlobalValue(conf.supybot.plugins.Sshd, 'port',
    Port(5922, """Determines the port number that the SSH server
    will run on. This must not be lower than 1024, or higher than 65535."""))

# Note this is not actually implemented!
conf.registerGlobalValue(conf.supybot.plugins.Sshd, 'interface',
    registry.String('', """Determines the interace that the SSH server will
    attempt to bind to. An empty value of '' will cause the server to attempt
    to bind all interfaces."""))

conf.registerGlobalValue(conf.supybot.plugins.Sshd, 'motd',
    registry.String('Welcome to the Secure Supybot Shell', """This is the
    message returned to clients on successful authorization"""))

conf.registerGlobalValue(conf.supybot.plugins.Sshd, 'capability',
    registry.String('owner', """Determines what capability (if any) the bot
    should require people trying to use this plugin to have."""))

conf.registerGlobalValue(conf.supybot.plugins.Sshd, 'rsaKeyFile',
    registry.String('id_rsa', """Determines the file name of the RSA key files. The
    private key file will be named as the configuration variable. The public key will
    be appended with ".pub", for example "id_rsa" and "id_rsa.pub"."""))

# New Lines:
# . SSH sends return key as as \r we send back \r\n because we usually want to
# .. display the new line.
# . Received new lines:
RNL = '\r'
# . Sent new lines:
SNL = '\r\n'

# Escape sequence:
ESC = ''.join(map(chr, [27, 91]))

# Backspace sequence:
# . Backspace-Space-Backspace.
BS = ''.join(map(chr, [8, 32, 8]))
    



# Converters

def sshCapable(irc, msg, args, state):
    capability = state.cb.registryValue('capability')
    if not ircdb.checkCapability(msg.prefix, capability):
        irc.errorNoCapability(capability, Raise=True)

def sshSource(irc, msg, args, state):
    # Messages fed via SSH are tagged with the connection.
    # Get the tag, or None.
    con = msg.tagged("fromSsh")
    if con:
        state.args.append(con)
    else:
        irc.error('This command may only be called from SSH connections',
                    Raise = True)

    
commands.addConverter('sshCapable', sshCapable)
commands.addConverter('sshSource', sshSource)

# The SSh Daemon Plugin Class
class Sshd(callbacks.Privmsg):

    # Used by example.
    threaded = True
    
    # In order to start the daemon, we must initialise the Twisted components.
    # . Realm
    # . Portal
    # . Checker
    # . Factory
    # Then the reactor must be set to listen, and run.
    def __init__(self):
        """ Instantiate the plugin and start the server """
        callbacks.Privmsg.__init__(self)
        
        # Keep track of connections for:
        # . Shutting down.
        # . Messaging over SSH.
        self.connections = []
        
        # Set a default irc object for cosmetic things like the prompt.
        self.irc = world.ircs[0]
        
        # Work out the full path to the key files.
        keypath = conf.supybot.directories.data.dirize(self.registryValue('rsaKeyFile'))
        
        # Build an SSH factory with the correct Twisted components.
        factory = build_SshTunnelFactory(keypath, self)
        
        if factory:
            # If creation was successful (ie keys were valid and parseable):
            # . Get the port number from the registry.
            port = self.registryValue('port')
            
            # . Sart listening and run the reactor.
            reactor.callLater(0, self.startListening, port, factory)
            #self.listener = reactor.listenTCP(port, factory)
            self.log.info('Started listening on port %s.' % port)
        else:
            # If the keys were bad:
            # . Tell the user, and tell them what to do about it.
            self.log.error('Your keys are unavailable, or broken. \
Please generate new ones by using the keygen command. You will need to reload \
the plugin after key generation')
   
    def startListening(self, port, factory):
        # This has to be called later for reload purposes
        self.log.critical('herel')
        self.listener = reactor.listenTCP(port, factory)            


    # logout()
    # . Plugin command to log the user out.
    # . Only available over SSH connections.
    def logout(self, irc, msg, args, con):
        """takes no arguments

        Log out of the SSH server.  This should only be called from an SSH
        connection, not from IRC.
        """
        # Grab the irc object for cosmetic reasons.
        self.irc = irc
        
        # I would like to do this, but it takes too long
        # irc.replySuccess('Logging out.')

        # Messages fed via SSH are tagged with the connection.
        # We know this exists, we passed it through the converter.
        msg.fromSsh.write_control('logging out...')
        
        # . Close it's connection.
        # . The connection will remove itself from the connection list when
        # .. it is closed.
        try:
            reactor.callLater(0, con.loseConnection)
        except Exception:
            self.log.error('Error closing connection. %s' % e)
    
    logout = commands.wrap(logout, ['sshSource'])
    

    # keygen()
    # . Plugin command to generate rsa key pairs.
    # . Only available to the owner
    # Modified from twisted.scripts.ckeygen
    def keygen(self, irc, msg, args, opts):
        """[--overwrite]
        
        Generate an RSA key pair. The optional [--overwrite] option is required
        to overwrite any existing keys. The keys will be generated in the default
        data directory, and will be called "id_rsa" (private), and "id_rsa.pub"
        (public).
        """
              
        # Get the filename from the configuration registry:
        filename = self.registryValue('rsaKeyFile')
        
        # Calculate the full path.
        filepath = conf.supybot.directories.data.dirize(filename)
        
        # Check if we were passed the [--overwrite] parameter.
        overwrite = len(opts) and opts[0][1]
        
        # Check if the file exists.
        if os.path.exists(filepath):
            # The private file exists:
            # . Check if overwrite has been specified.
            if not overwrite:
                # --overwrite has not been specified, but the files exist:
                # . Report an error to the user and return.
                irc.error('The file already exists, please run the command \
with the "--overwrite" option if you wish to over write the existing keys.')
                return

        # Otherwise, generate the keys.
        self.log.debug('Generating public/private rsa key pair.')
        
        # (Modified from twisted.scripts.ckeygen)
        # Generate a key object.
        key = RSA.generate(1024, common.entropy.get_bytes)        
        
        # Create and write the private key file.
        # . Generate the string.
        privk = keys.makePrivateKeyString(key)
        # . Write the file
        privf = open(filepath, 'w')
        privf.write(privk)
        privf.close()
        # . Fix the permissions
        os.chmod(filepath, 33152)
        
        # Create and write the public key file.
        # . Generate the string.
        pubk = keys.makePublicKeyString(key)
        # . Write the file.
        pubf = open('%s.pub' % filepath, 'w')
        pubf.write(pubk)
        pubf.close()
        
        # Reply success to the user, and a copy of the fingerprint for the newly
        # . generated keys.
        irc.replySuccess(self.get_fingerprint())
        
    # Wrap the keygen() method.
    # . In a separate thread.
    # . Checking for owner capability.
    # . with the optional --overwrite parameter    
    keygen = commands.thread(commands.wrap(keygen,
                                            ['owner',
                                            commands.getopts({'overwrite': ''})]))
                                            
    # fingerprint()
    # . Plugin cmomand to display the RSA key fingerprint.
    # . Can be called by users with the plugins.Sshd.capability capability.
    #TODO: make this a non-channel command?
    def fingerprint(self, irc, msg, args):
        """takes no arguments
        
        Prints the fingerprint for the rsa public key.
        """
        
        irc.reply(self.get_fingerprint())
    
    # Wrap the fingerprint() method:
    # . In a separate thread.
    # . Checking for the capability with the sshCapable converter
    fingerprint = commands.thread(commands.wrap(fingerprint, ['sshCapable']))
    
    
    # sshusers()
    # . Plugin command to display the users connected to the SSh daemon.
    # . Can be called by users with the plugins.Sshd.capability capability.
    def sshusers(self, irc, msg, args):
        """takes no arguments
        
        Prints a list of users connected by SSH.
        """
        
        # . Build a pretty list:
        # AA Need to know how to use the formatter for lists !!
        u =  ['%s' % (c.username) for c in self.connections if c.major]
        m = 'Users connected by SSH (%s): %s' % (len(u), ', '.join(u))
        # . Reply to the user.
        irc.reply(m)
        
    
    sshusers = commands.wrap(sshusers, ['sshCapable'])
    
    
    def say(self, irc, msg, args, target, text):
        """<username> <text>
            
        Say <text> to user <username>.
        """
         
        # Get the connection (if over SSH).
        con = msg.fromSsh
        nick = ''
        if con:
            nick = con.username
        else:
            nick = msg.nick
        
        if nick == target:
            irc.error('You can not send messages to yourself.', Raise=True)
            
        found = False
        for c in self.connections:
            if c.username == target:
                if not c is con:
                    found = True
                    t = '%s to %s: %s' % (nick, target, text)
                    c.write_major(t)
        if found:
            irc.replySuccess(t)
        else:
            irc.error('The requested user/nick is not connected to SSH.')

    say = commands.wrap(say, ['sshCapable','something', 'text'])
    
    ###
    # sshsayall()
    # . Plugin command to send a message to all users connected to Ssh.
    # . Can be called by users with the plugins.Sshd.capability capability.
    def wall(self, irc, msg, args, text):
        """<text>
        
        Say <text> to all users connected to SSH
        """
        # Get the connection which sent the request (if over SSH) since it needs 
        # special treatment.
        con = msg.tagged("fromSsh")
        nick = None
        if con:
            nick = con.username
        else:
            nick = msg.nick
        # Generate a pretty message.
        t = '%s to all: %s' % (nick, text)
        found = False
        # If there are any connections or the message was not sent from the only
        # . connection:
        if len(self.connections) and self.connections != [con]:
            # Iterate through all the connections:
            for c in self.connections:
                # If not the connection that sent the original query:
                if not c is con:
                    c.write_major(t)
                    found = True
        # Reply a copy and success message to the user.
        if found:
            irc.replySuccess(t)    
        # There are no SSH connections.
        else:
            # Inform the user of the error and return.
            irc.error('There are no (other) current SSh connections.')
            
    # Wrap the method with a single required parameter
    wall = commands.wrap(wall, ['sshCapable', 'text'])
    

    # get_fingerprint()
    # . Utility method:
    # .. Extract the fingerprint from the public key file.
    # .. Return it as a pretty string.
    def get_fingerprint(self):
        """ generate the fingerprint from the key file and return it with a caption """
        
        # Get the key file filename from the registry.
        filename = self.registryValue('rsaKeyFile')
        
        # Calculate the full path
        filepath = '%s.pub' % conf.supybot.directories.data.dirize(filename)
        
        # Open and read the public key file.
        fd = open(filepath)
        publickey = fd.read()
        fd.close()
        
        # Arrange the key digest.
        # . (taken from twisted.scripts.ckeygen)
        fp = ':'.join(['%02x' % ord(x) for x in md5.new(publickey).digest()])
        
        # Return the string with a caption.
        return 'The rsa public key fingerprint is %s' % fp
            
    # This is called when the module is unloaded
    # . To close the server, we must:
    # .. Close all the active connections
    # .. Stop listening
    def die(self):
        """ closes the server down """
        # Close down each individual connection.
        for con in self.connections:
            # Add the loseConnection call to the reactor queue.
            reactor.callLater(0, con.loseConnection)
        # Stop the daemon from listening
        reactor.callLater(0, self.listener.stopListening)
        # Inform the user.
        self.log.info('Stopping server.')

    # This is called by the protocol class on receipt of an entire line. The
    # protocol class sends us a reference to itself so that we can tag
    # outbound messages for picking up replies.
    # After tagging the message is fed to the stream.
    def receivedcommand(self, cmd, con):
        """ handle a single command """
        self.log.debug('Received command: %s from %s', cmd, con.hostmask)
        m =  ircmsgs.privmsg(self.irc.nick, cmd.strip(), con.hostmask)
        m.tag('fromSsh', con)
        self.irc.feedMsg(m)

    # The Outfilter does two things:
    #   1 Intercept messages as replies to ones sent over SSH.
    #   2 Intercept messages sent from other sources to SSH.
    #
    #   Messages sent from SSH are tagged with the instance of the SSH connection.
    #   This is picked up in the replyTo tag and used to directly send the message.
    #
    #   Messages sent to SSH are tagged with the SSH user name. This is looked up
    #   against the list of connected users, and the appropriate message sent. 
    def outFilter(self, irc, msg):
        """ filter outbound messages for our reply """
        
        if msg.forSsh:
            # The message has been tagged for SSH.
            for c in self.connections:
                # Check the connections for the user.
                if c.username == msg.forSsh:
                    # Write to the user's major channel(s).
                    c.write_major(msg.args[1])
            # Stop processing of the message.
            return

        elif msg.inReplyTo:
            # The message is a reply to a message.
            if msg.inReplyTo.fromSsh:
                # The message is in reply to one sent from SSH.
                # Use the tagged connection instance to write the reply.
                msg.inReplyTo.fromSsh.write_reply(msg.args[1])
                # Register debug output like other plugins do on reply.
                self.log.debug('Replying with %s.',msg.args[1])
                # will replace with utils.quoted(msg.args[1])
                # Stop processing ot the message.
                return
                
        # Otherwise do nothing and pass the message on to the other filters.
        return msg

    
    # When a new connection is made:
    #   It is added to the list of connections.
    #   A message is sent to all the other connected users major channels.
    #   TODO: Make this configurable? eg informAllOnConnect
    def add_connection(self, con):
        """ Called when a new connection is started """
        if con.major:
            # Only inform other users of new major channels.
            for c in self.connections:
                # Echo the connected host and username.
                c.write_control('%s on %s has connected to SSH.' % (con.username,
                                    con.peer.host))
        self.connections.append(con)
    
    # When a connection is closed:
    #   It is removed from the list of connections.
    #   A message is sent to all the other connected users major channels.
    #   TODO: Make this configurable?
    def remove_connection(self, con):
        self.connections.remove(con)
        for c in self.connections:
            c.write_control('%s on %s has disconnected from SSH.' % (con.username,
                                con.peer.host))   
        
class SBProtocol(protocol.Protocol):
    """ the protocol that gets wrapped in SSH """
    # On a successful connection we need to:
    #     **set our hostmask by generating it now done by the avatar
    #     register ourselves with the plugin
    #     set up a buffer for reading into
    #     get our user information from the avatar
    #     add our user to the user database with the current hostmask
    #     Write the message of the day to the client
    def connectionMade(self):
        """ called on connection """
        self.cb.log.info('Connection Opened from %s:%s',
                                self.peer.host, self.peer.port)
        self.rbuf = ''
        self.cb.add_connection(self)

        # Add them to the user db and welcome them
        # (we already know this is a valid user to allow login)
        self.user = ircdb.users.getUser(self.username)
        self.user.addAuth(self.hostmask)
        
        # send the prompt and welcome to the user.
        # this has been fixed to use a deferred call for putty
        # though this is the correct implementation.
        self.write_motd()
        

    # On losing our connection we need to:
    #     remove ourselves from the plugins list of connections
    #     unregister the user with the hostmask
    def connectionLost(self, reason):
        """ Called on loss of connection. """
        self.cb.log.info('Connection Lost to %s:%s',
                                self.peer.host, self.peer.port)
        # JJJ You might want to log where the connection was from.
        self.cb.remove_connection(self)
   
        # remove the hostmask!
        # !! This is a bug !!
        # It is clearing the whole stuff rather than just the hostmask.
        # Should be easy to fix, just need to sit down and work out how
        self.user.clearAuth()

    def loseConnection(self):
        # This needed to be added because of a Twisted feature
        # Normal Twisted the following would do it
        self.transport.loseConnection()
        # However the Ssh transport is actually a dummy
        # The real connection (from the avatar) needs to be closed
        #self.loseRealConnection()
        
    # On receiving data:
    #     __processdata() is called which appends to the read buffer.
    #     __processreadbuf() is called which checks the readbuffer.
    #     (if the readbuffer is non-zero)
    #     (close the connection if the read buffer exceeds 512B in length.
    def dataReceived(self, data):
        """ called on receipt of data """
        self._processdata(data)
        rl = len(self.rbuf)
        if rl:
            if rl > 512:
                # Close the connecion if the read buffer is getting too large.
                self.loseConnection()
            else:
                # Otherwise process it.
                self._processreadbuf()

    def _processdata(self, data):
        """ Process individual characters in the stream """
        for c in data:
            cc = ord(c)
            doname = '_receivedchr_%s' % cc
            
            if hasattr(self, doname):
                # we have a special handler for this character
                getattr(self, doname)()
            else:
                # this this a general character
                self._receivedchr_general(c)

    # process the data in the read buffer
    # if there is one or more lines, call __linereceived()
    def _processreadbuf(self):
        lines = self.rbuf.split(RNL)
        if len(lines) > 1:
            # we have a return
            self.rbuf = ''
            for l in lines:
                # remove the last element, will be empty
                if lines[-1] == '':
                    lines.pop()
                self._linereceived(l)

    # Called when we receive a line
    # In our case, a line is a command so we send it to the plugin (cb)
    def _linereceived(self, cmd):
        """ Received a line of data """
        if len(cmd):
            self.cb.receivedcommand(cmd, self)
        self.write(SNL)
        self.write_prompt()

    # The general character handler
    def _receivedchr_general(self, c):
        """ Handle all other keys """
        # We want to add it to the read buffer and echo it back
        self.rbuf = '%s%s' % (self.rbuf, c)
        if self.echo:
            self.write(c)
       
    # The special character handlers
    def _receivedchr_3(self):
        """ Control-C handler """
        # We want to escape the line and clear the buffer.
        self.transport.write(SNL)
        self.rbuf = ''
        self.write_prompt()

    def _receivedchr_4(self):
        """ Control-D handler """
        # We want to log out.
        self._linereceived('logout')

    def _receivedchr_13(self):
        """ Return key handler """
        # We want to add the 13 to the buffer, but echo back a full newline.
        if len(self.rbuf):
            self.rbuf = '%s%s' % (self.rbuf, RNL)
    
    # 27 and 91 handle the escapes.
    # We need to add them to the read buffer, but not echo them back if 91 is after
    # 27, or if 65-68 are after 27 and 91.
    def _receivedchr_27(self):
        self.rbuf = '%s%s' % (self.rbuf, chr(27))
        
    def _receivedchr_91(self):
        if self.rbuf.endswith(chr(27)):
            self.rbuf = '%s%s' % (self.rbuf, chr(91))
        else:
            self._receivedchr_general(chr(91))
        
    # Direction key handlers just stop the user moving around at the moment. 
    def _receivedchr_65(self):
        """ Up key handler """
        # does nothing but may do history
        if self.rbuf.endswith(ESC):
            self.rbuf = self.rbuf[:-2]
        else:
            self._receivedchr_general(chr(65))
    
    def _receivedchr_66(self):
        """ Down key handler """
        # does nothing but may do history
        if self.rbuf.endswith(ESC):
            self.rbuf = self.rbuf[:-2]
        else:
            self._receivedchr_general(chr(66))
                    
    def _receivedchr_67(self):
        """ Left key handler """
        # does nothing
        if self.rbuf.endswith(ESC):
            self.rbuf = self.rbuf[:-2]
        else:
            self._receivedchr_general(chr(67))
                    
    def _receivedchr_68(self):
        """ Left key handler """
        # does nothing
        if self.rbuf.endswith(ESC):
            self.rbuf = self.rbuf[:-2]
        else:
            self._receivedchr_general(chr(68))
       
    def _receivedchr_127(self):
        """ backspace handler """
        # eek, backspace!  what a mess, seems to work though.
        # If the read buffer is longer than 0, put a backspace,
        # then a space, then a backspace (move, clear, move)
        # then shorten the read buffer by one
        if len(self.rbuf) > 0:
            self.write(BS)
            self.rbuf = self.rbuf[:-1]
    
    # write data to the transport
    # Use a deferred call, so everything has run its course first
    def write(self, msg):
        reactor.callLater(0, self.transport.write, msg)
    
    def write_reply(self, msg=''):
        if self.echo:
            msg = '%s%s%s' %  (SNL, msg, SNL)
        self.write(msg)
        self.write_prompt()
        
    def write_control(self, msg):
        if not self.echo:
            msg = '*%s' % msg
        self.write_major(msg)
    
    def write_major(self, msg):
        if self.major:
            self.write_reply(msg)
         
    # Write the MOTD (message of the day) to the client
    def write_motd(self):
        #if self.echo:
        self.write_control(self.cb.registryValue('motd'))

    # Write the prompt *and* the contents of the read buffer to the client
    def write_prompt(self):
        if self.echo:
            self.write('%s@%s: $ %s' % \
        (self.username, self.cb.irc.nick, self.rbuf))

class SshTunnelSession(object):
    """ class representing each connected SSH client

    I expect I can do a lot of terminal specific things in here which might
    be cool"""
    def __init__(self, avatar):
        """ store the avatar for later use """
        # Decides whether the session will echo
        #For the log.
        self.cb = avatar.cb
        self.echo = True
        self.major = True
        self.avatar = avatar

    def getPty(self, term, windowSize, attrs):
        pass

    def closed(self):
        self.ep.loseConnection()
        
    def execCommand(self, proto, cmd):
        if cmd == 'noecho':
            # handle the noecho command
            self.echo = not self.echo
            if hasattr(self, 'ep'):
                self.ep.echo = self.echo
            self.cb.log.debug('Echoing set to: %s', self.echo)
        elif cmd == 'nomajor':
            self.major = not self.major
            if hasattr(self, 'ep'):
                self.ep.major = self.major
            self.cb.log.debug('Major set to: %s', self.major)    
            
        else:
            self.cb.log.debug('Bad SSH command: %s', cmd)

# Instantiate the Protocol to talk to the bot by:
#     connect it to the transport that we were passed, and
#     wrapping that connection in our SSH session
#TODO: Understand this better :)
    def openShell(self, trans):
        """ called back on a successful connection """
        
        # Instantiate the protocol.
        ep = SBProtocol()
        self.ep = ep       
        # Pass on the useful information from the avatar to the protocol.
        # The callback, an instance of the plugin.
        ep.cb = self.avatar.cb
        
        # Set the default to echo
        ep.echo = self.echo
        
        # Set the defaul to be a major channel'
        ep.major = self.major
        
        # Hacks based on the fact that Twisted gives us a dummy transport.
        # self.avatar.conn.transport.transport is a real transport
        
        # the peer information
        ep.peer = self.avatar.conn.transport.transport.getPeer()
        
        # The connection closing method rescued.
        # The session's loseConnection is all wrong, so it is overridden with the
        # . method from the actual transport.
        ep.loseRealConnection = self.avatar.conn.transport.transport.loseConnection
        
        # The username the user logged in with.
        ep.username = self.avatar.username
        
        # Build the hostmask
        ep.hostmask = self.build_hostmask(ep)
        
        # Twisted black magic
        ep.makeConnection(trans)
        trans.makeConnection(session.wrapProtocol(ep))
        
    ###
    # build_hostmask()
    # Utility method to builf a part-random hostmask for use with SSh connections.
    # . To generate the hostmask:
    # .. <username><randomstring>!local@<serverName>
    # . The random element is to ensure no duplicate hostmasks.
    # move this to be on it's own
    def build_hostmask(self, ep):
        """ build a new partly random hostmask and return it """
        
        # Create a temproary string and split it:
        ts = utils.mktemp()[:9]
        
        # Join the individual elements.
        hm = '%s!%s@Sshd.%s' % (ts, ep.username, ep.peer.host)
        
        self.cb.log.debug('Generated random hostmask %s.', hm)
        return hm
   
        

# These are the classes that are needed to work twisted.cred, the
# authentication system used by Twisted.
# They comprise the Checkers, the Avatar, and the Realm.
# Details on what each of these classes do and how they fit is at
# http://twistedmatrix.com/documents/current/howto/cred

# The Realm
# On successful authorisation, the portal will request from the realm an
# avatar for the connection just made.
# Should we deal with the hostmask here?

class SshRealm:
    """ connects the avatars to the SSH session """
    __implements__ = portal.IRealm,

    def requestAvatar(self, avatarId, mind, *interfaces):
        """ called back on a succesful connection """
        # Instantiate the avatar with the username and reference to the plugin instance
        return interfaces[0], SshAvatar(avatarId, self.cb), lambda: None

# The Avatar
# . This is what gets returned to the session after a successful connection.
# .. In this case, the business logic is just username information, and a
# .. reference to the plugin to call back to. Since the tunneled connection will
# .. be passed the avatar, we could use it to sotre anything that the connection
# .. will need to know.
class SshAvatar(avatar.ConchUser):
    """ a 'unit of business logic' """
    def __init__(self, username, cb):
        """ instantiate the avatar, and set the data we care about """
        avatar.ConchUser.__init__(self)
        self.username = username
        self.cb = cb
        #self.build_hostmask()
        self.channelLookup.update({'session':session.SSHSession})
        
   

# The Checkers
# Public key checker
# . Does nothing. Not ever instantiated.
class InMemoryPublicKeyChecker(SSHPublicKeyDatabase):
    """ Public key checker """
    def checkKey(self, credentials):
        return True
        
# Password Checker
# Twisted.conch.ssh uses individual credentials matching. This class says
# that it will match a username/password combination as it is in the
# credentialInterfaces list. This is just a dummy, because we will not
# use a Twisted Credewntials at all, we will get our information from
# Supybot.

# JJJ Is there any reason why this shouldn't be a new-style class?  We try to
#     use those exclusively in Supybot.
# AA I thought this is a new-style class.
class SshChecker(object):
    """ SSH Username and Password Credential checker """
    # this implements line tells the portal that we can handle un/pw
    __implements__ = (checkers.ICredentialsChecker,)
    credentialInterfaces = (credentials.IUsernamePassword,)

    # Taken from identify() in User.py
    # (but can't raise errors, have to return the error that conch is
    # expecting)
    # Get the user object to check it
    # (Failed login if the user does not exist)
    # Check capability for that user.
    # (Failed log in if it does not exist)
    # Check the password and return the username as an Avatar ID
    # (Failed login if the password is wrong)
    def requestAvatarId(self, credentials):
        """ Return an avatar id or return an error """
        username = None
        user = None
        try:
            # first get the user (ensuring she exists)
            user = ircdb.users.getUser(credentials.username)
        except KeyError:
            return failure.Failure(error.UnauthorizedLogin())
        # check the capability of the user
        cap = self.cb.registryValue('capability')
        # there is a required capability to connect
        if cap:
            # check the user has the capability
            # this is the correct way, never use user.checkCapability
            if not ircdb.checkCapability(credentials.username, cap):
                return failure.Failure(error.UnauthorizedLogin())
        # check the user has the correct password
        if user.checkPassword(credentials.password):
            # password is good, set the avatar id to be returned
            return credentials.username
        else:
            return failure.Failure(error.UnauthorizedLogin())
                   

class SshTunnelFactory(factory.SSHFactory):
    services = {
        'ssh-userauth': userauth.SSHUserAuthServer,
        'ssh-connection': connection.SSHConnection
    }

# Ssh Factory factory function
# . Set up some class attributes of the SshTunnelFactory based on data given to
# .. us by the plugin instance. These are the portal, and the keys.
def build_SshTunnelFactory(keypath, cb):
    """ build the right protocol factory or raise an exception if no keys """
    
    # Calculate the path of the public key from the private key by appending '.pub'
    pubpath = '%s.pub' % keypath
    
    # Check that both private and public keys exist.
    if os.path.exists(keypath) and os.path.exists(pubpath):
        # The keys exist:
        
        # . The Realm
        realm = SshRealm()
        # .. Set this instance as an attribute.
        realm.cb = cb
        
        # . The Portal
        ssh_portal = portal.Portal(realm)
        
        # . The Checker
        checker = SshChecker()
        # .. Set this instance as an attribute.
        checker.cb = cb
        # .. Register the Checker with the Portal.
        ssh_portal.registerChecker(checker)
        # Set the class for modifying.
        f = SshTunnelFactory
        
        # Set the portal for the class.
        f.portal = ssh_portal
        
        # set the keys for the class
        f.publicKeys = {'ssh-rsa': keys.getPublicKeyString(filename=pubpath)}
        f.privateKeys = {'ssh-rsa': keys.getPrivateKeyObject(filename=keypath)}
        
        # Instantiate the factory and return it
        return f()
    else:
        # The keys do not exist:
        # . Return None.
        return None

# Register components with Twisted.
components.registerAdapter(SshTunnelSession, SshAvatar, session.ISession)

# Set the class for the plugin.
Class = Sshd

# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
