Added SSH support, based on patch by hagna (issue 166).

This commit is contained in:
Griatch 2011-05-27 17:47:35 +00:00
parent d2400a8a6b
commit 7c56c69cea
7 changed files with 445 additions and 102 deletions

View file

@ -5,99 +5,80 @@ with Evennia's permissions system.
To call these locks, make sure this module is included in the
settings tuple PERMISSION_FUNC_MODULES then define a lock on the form
'<access_type>:func(args)' and add it to the object's lockhandler.
Run the check method of the handler to execute the lock check.
Run the access() method of the handler to execute the lock check.
Note that accessing_obj and accessed_obj can be any object type
with a lock variable/field, so be careful to not expect
a certain object type.
Appendix: MUX locks
Below is a list nicked from the MUX help file on the locks available
in standard MUX. Most of these are not relevant to core Evennia since
locks in Evennia are considerably more flexible and can be implemented
on an individual command/typeclass basis rather than as globally
available like the MUX ones. So many of these are not available in
basic Evennia, but could all be implemented easily if needed for the
individual game.
MUX locks
Below is a list nicked from the MUX docs on the locks available
in MUX. These are not all necessarily relevant to an Evennia game
but to show they are all possible with Evennia, each entry is a
suggestion on how one could implement similar functionality in Evennia.
Name: Affects: Effect:
-------------------------------------------------------------------------
MUX Name: Affects: Effect:
-------------------------------------------------------------------------------
DefaultLock: Exits: controls who may traverse the exit to
its destination.
Evennia: specialized permission key
'traverse' checked in move method
Evennia: "traverse:<lockfunc()>"
Rooms: controls whether the player sees the SUCC
or FAIL message for the room following the
room description when looking at the room.
Evennia: This is better done by implementing
a clever room class ...
Evennia: Custom typeclass
Players/Things: controls who may GET the object.
Evennia: specialized permission key 'get'
defined on object, checked by get command
Evennia: "get:<lockfunc()"
EnterLock: Players/Things: controls who may ENTER the object
Evennia: specialized permission key 'enter'
defined on object, checked by move command
Evennia:
GetFromLock: All but Exits: controls who may gets things from a given
location.
Evennia: Probably done best with a lock function
that searches the database for permitted users
Evennia:
GiveLock: Players/Things: controls who may give the object.
Evennia: specialized permission key 'give'
checked by the give command
Evennia:
LeaveLock: Players/Things: controls who may LEAVE the object.
Evennia: specialized permission key 'leave'
checked by move command
Evennia:
LinkLock: All but Exits: controls who may link to the location if the
location is LINK_OK (for linking exits or
setting drop-tos) or ABODE (for setting
homes)
Evennia: specialized permission key 'link'
set on obj and checked by link command
Evennia:
MailLock: Players: controls who may @mail the player.
Evennia: Lock function that pulls the
config from the player to see if the
calling player is on the blacklist/whitelist
Evennia:
OpenLock: All but Exits: controls who may open an exit.
Evennia: specialized permission key 'open'
set on exit, checked by open command
Evennia:
PageLock: Players: controls who may page the player.
Evennia: see Maillock
Evennia: "send:<lockfunc()>"
ParentLock: All: controls who may make @parent links to the
object.
Evennia: This is handled with typeclasses
and typeclass switching instead.
Evennia: Typeclasses and "puppet:<lockstring()>"
ReceiveLock: Players/Things: controls who may give things to the object.
Evennia: See GiveLock
Evennia:
SpeechLock: All but Exits: controls who may speak in that location
Evennia: Lock function checking if there
is some special restrictions on the room
(game dependent)
Evennia:
TeloutLock: All but Exits: controls who may teleport out of the
location.
Evennia: See LeaveLock
Evennia:
TportLock: Rooms/Things: controls who may teleport there
Evennia: See EnterLock
Evennia:
UseLock: All but Exits: controls who may USE the object, GIVE the
object money and have the PAY attributes
run, have their messages heard and possibly
acted on by LISTEN and AxHEAR, and invoke
$-commands stored on the object.
Evennia: Implemented per game
Evennia: Commands and Cmdsets.
DropLock: All but rooms: controls who may drop that object.
Evennia: specialized permission key 'drop'
set on room, checked by drop command.
Evennia:
VisibleLock: All: Controls object visibility when the object
is not dark and the looker passes the lock.
In DARK locations, the object must also be
set LIGHT and the viewer must pass the
VisibleLock.
Evennia: Better done with Scripts implementing
a dark state/cmdset. For a single object,
use a specialized permission key 'visible'
set on object and checked by look command.
Evennia: Room typeclass with Dark/light script
"""
from django.conf import settings

View file

@ -39,9 +39,11 @@ SERVERNAME = settings.SERVERNAME
VERSION = get_evennia_version()
TELNET_PORTS = settings.TELNET_PORTS
SSH_PORTS = settings.SSH_PORTS
WEBSERVER_PORTS = settings.WEBSERVER_PORTS
TELNET_ENABLED = settings.TELNET_ENABLED and TELNET_PORTS
SSH_ENABLED = settings.SSH_ENABLED and SSH_PORTS
WEBSERVER_ENABLED = settings.WEBSERVER_ENABLED and WEBSERVER_PORTS
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
IMC2_ENABLED = settings.IMC2_ENABLED
@ -149,6 +151,8 @@ class Evennia(object):
print ' %s (%s) started on port(s):' % (SERVERNAME, VERSION)
if TELNET_ENABLED:
print " telnet: " + ", ".join([str(port) for port in TELNET_PORTS])
if SSH_ENABLED:
print " ssh: " + ", ".join([str(port) for port in SSH_PORTS])
if WEBSERVER_ENABLED:
clientstring = ""
if WEBCLIENT_ENABLED:
@ -203,6 +207,18 @@ if TELNET_ENABLED:
telnet_service.setName('EvenniaTelnet%s' % port)
EVENNIA.services.addService(telnet_service)
if SSH_ENABLED:
from src.server import ssh
for port in SSH_PORTS:
factory = ssh.makeFactory({'protocolFactory':ssh.SshProtocol,
'protocolArgs':()})
ssh_service = internet.TCPServer(port, factory)
ssh_service.setName('EvenniaSSH%s' % port)
EVENNIA.services.addService(ssh_service)
if WEBSERVER_ENABLED:
# a django-compatible webserver.

332
src/server/ssh.py Normal file
View file

@ -0,0 +1,332 @@
"""
This module implements the ssh (Secure SHell) protocol for encrypted
connections.
This depends on a generic session module that implements
the actual login procedure of the game, tracks
sessions etc.
"""
import os
from twisted.cred.checkers import credentials
from twisted.cred.portal import Portal
from twisted.conch.ssh.keys import Key
from twisted.conch.interfaces import IConchUser
from twisted.conch.ssh.userauth import SSHUserAuthServer
from twisted.conch.ssh import common
from twisted.conch.insults import insults
from twisted.conch.manhole_ssh import TerminalRealm, _Glue, ConchFactory
from twisted.conch.manhole import Manhole, recvline
from twisted.internet import defer
from django.conf import settings
from src.server import session
from src.utils import ansi, utils, logger
ENCODINGS = settings.ENCODINGS
CTRL_C = '\x03'
CTRL_D = '\x04'
CTRL_BACKSLASH = '\x1c'
CTRL_L = '\x0c'
class SshProtocol(Manhole, session.Session):
"""
Each player connecting over ssh gets this protocol assigned to
them. All communication between game and player goes through
here.
"""
def terminalSize(self, width, height):
"""
Initialize the terminal and connect to the new session.
"""
# Clear the previous input line, redraw it at the new
# cursor position
self.terminal.eraseDisplay()
self.terminal.cursorHome()
self.width = width
self.height = height
# initialize the session
self.session_connect(self.getClientAddress())
def connectionMade(self):
"""
This is called when the connection is first
established.
"""
recvline.HistoricRecvLine.connectionMade(self)
self.keyHandlers[CTRL_C] = self.handle_INT
self.keyHandlers[CTRL_D] = self.handle_EOF
self.keyHandlers[CTRL_L] = self.handle_FF
self.keyHandlers[CTRL_BACKSLASH] = self.handle_QUIT
def handle_INT(self):
"""
Handle ^C as an interrupt keystroke by resetting the current input
variables to their initial state.
"""
self.lineBuffer = []
self.lineBufferIndex = 0
self.terminal.nextLine()
self.terminal.write("KeyboardInterrupt")
self.terminal.nextLine()
def handle_EOF(self):
"""
Handles EOF generally used to exit.
"""
if self.lineBuffer:
self.terminal.write('\a')
else:
self.handle_QUIT()
def handle_FF(self):
"""
Handle a 'form feed' byte - generally used to request a screen
refresh/redraw.
"""
self.terminal.eraseDisplay()
self.terminal.cursorHome()
def handle_QUIT(self):
"""
Quit, end, and lose the connection.
"""
self.terminal.loseConnection()
def connectionLost(self, reason=None, step=1):
"""
This is executed when the connection is lost for
whatever reason.
Closing the connection takes two steps
step 1 - is the default and is used when this method is
called automatically. The method should then call self.session_disconnect().
Step 2 - means this method is called from at_disconnect(). At this point
the sessions are assumed to have been handled, and so the transport can close
without further ado.
"""
insults.TerminalProtocol.connectionLost(self, reason)
if step == 1:
self.session_disconnect()
else:
self.terminal.loseConnection()
def getClientAddress(self):
"""
Returns the client's address and port in a tuple. For example
('127.0.0.1', 41917)
"""
return self.terminal.transport.getPeer()
def lineReceived(self, string):
"""
Communication Player -> Evennia. Any line return indicates a
command for the purpose of the MUD. So we take the user input
and pass it on to the game engine.
"""
self.at_data_in(string)
def lineSend(self, string):
"""
Communication Evennia -> Player
Any string sent should already have been
properly formatted and processed
before reaching this point.
"""
for line in string.split('\n'):
self.terminal.write(line) #this is the telnet-specific method for sending
self.terminal.nextLine()
# session-general method hooks
def at_connect(self):
"""
Show the banner screen.
"""
self.telnet_markup = True
# show connection screen
self.execute_cmd('look')
def at_login(self, player):
"""
Called after authentication. self.logged_in=True at this point.
"""
if player.has_attribute('telnet_markup'):
self.telnet_markup = player.get_attribute("telnet_markup")
else:
self.telnet_markup = True
def at_disconnect(self, reason="Connection closed. Goodbye for now."):
"""
Disconnect from server
"""
char = self.get_character()
if char:
char.at_disconnect()
self.at_data_out(reason)
self.connectionLost(step=2)
def at_data_out(self, string, data=None):
"""
Data Evennia -> Player access hook. 'data' argument is ignored.
"""
try:
string = utils.to_str(string, encoding=self.encoding)
except Exception, e:
self.lineSend(str(e))
return
nomarkup = not self.telnet_markup
raw = False
if type(data) == dict:
# check if we want escape codes to go through unparsed.
raw = data.get("raw", self.telnet_markup)
# check if we want to remove all markup
nomarkup = data.get("nomarkup", not self.telnet_markup)
if raw:
self.lineSend(string)
else:
self.lineSend(ansi.parse_ansi(string, strip_ansi=nomarkup))
def at_data_in(self, string, data=None):
"""
Line from Player -> Evennia. 'data' argument is not used.
"""
try:
string = utils.to_unicode(string, encoding=self.encoding)
self.execute_cmd(string)
return
except Exception, e:
logger.log_errmsg(str(e))
class ExtraInfoAuthServer(SSHUserAuthServer):
def auth_password(self, packet):
"""
Password authentication.
Used mostly for setting up the transport so we can query
username and password later.
"""
password = common.getNS(packet[1:])[0]
c = credentials.UsernamePassword(self.user, password)
c.transport = self.transport
return self.portal.login(c, None, IConchUser).addErrback(
self._ebPassword)
class AnyAuth(object):
"""
Special auth method that accepts any credentials.
"""
credentialInterfaces = (credentials.IUsernamePassword,)
def requestAvatarId(self, c):
"Generic credentials"
up = credentials.IUsernamePassword(c, None)
username = up.username
password = up.password
src_ip = str(up.transport.transport.getPeer().host)
return defer.succeed(username)
class TerminalSessionTransport_getPeer:
"""
Taken from twisted's TerminalSessionTransport which doesn't
provide getPeer to the transport. This one does.
"""
def __init__(self, proto, chainedProtocol, avatar, width, height):
self.proto = proto
self.avatar = avatar
self.chainedProtocol = chainedProtocol
session = self.proto.session
self.proto.makeConnection(
_Glue(write=self.chainedProtocol.dataReceived,
loseConnection=lambda: avatar.conn.sendClose(session),
name="SSH Proto Transport"))
def loseConnection():
self.proto.loseConnection()
def getPeer():
session.conn.transport.transport.getPeer()
self.chainedProtocol.makeConnection(
_Glue(getPeer=getPeer, write=self.proto.write,
loseConnection=loseConnection,
name="Chained Proto Transport"))
self.chainedProtocol.terminalProtocol.terminalSize(width, height)
def getKeyPair():
"""
This function looks for RSA keypair files in the current directory. If they
do not exist, the keypair is created.
"""
if not (os.path.exists('ssh-public.key') and os.path.exists('ssh-private.key')):
# No keypair exists. Generate a new RSA keypair
print " Generating SSH RSA keypair (only done once) ...",
from Crypto.PublicKey import RSA
KEY_LENGTH = 1024
rsaKey = Key(RSA.generate(KEY_LENGTH))
publicKeyString = rsaKey.public().toString(type="OPENSSH")
privateKeyString = rsaKey.toString(type="OPENSSH")
# save keys for the future.
file('ssh-public.key', 'w+b').write(publicKeyString)
file('ssh-private.key', 'w+b').write(privateKeyString)
print " done."
else:
publicKeyString = file('ssh-public.key').read()
privateKeyString = file('ssh-private.key').read()
return Key.fromString(publicKeyString), Key.fromString(privateKeyString)
def makeFactory(configdict):
"""
Creates the ssh server factory.
"""
def chainProtocolFactory():
return insults.ServerProtocol(
configdict['protocolFactory'],
*configdict.get('protocolConfigdict', ()),
**configdict.get('protocolKwArgs', {}))
rlm = TerminalRealm()
rlm.transportFactory = TerminalSessionTransport_getPeer
rlm.chainedProtocolFactory = chainProtocolFactory
factory = ConchFactory(Portal(rlm))
# create/get RSA keypair
publicKey, privateKey = getKeyPair()
factory.publicKeys = {'ssh-rsa': publicKey}
factory.privateKeys = {'ssh-rsa': privateKey}
factory.services = factory.services.copy()
factory.services['ssh-userauth'] = ExtraInfoAuthServer
factory.portal.registerChecker(AnyAuth())
return factory

View file

@ -36,6 +36,10 @@ WEBSERVER_PORTS = [8000]
# Start the evennia ajax client on /webclient
# (the webserver must also be running)
WEBCLIENT_ENABLED = True
# Activate SSH protocol
SSH_ENABLED = False
# Ports to use for SSH
SSH_PORTS = [8022]
# Activate full persistence if you want everything in-game to be
# stored to the database. With it set, you can do typeclass.attr=value
# and value will be saved to the database under the name 'attr'.

View file

@ -9,7 +9,7 @@
<link rel='stylesheet' type="text/css" media="screen" href="/media/css/webclient.css">
<!-- Importing the online jQuery javascript library -->
<script src="http://code.jquery.com/jquery-1.4.4.js" type="text/javascript" charset="utf-8"></script>
<script src="http://code.jquery.com/jquery-1.6.1.js" type="text/javascript" charset="utf-8"></script>
<!--for offline testing, download the jquery library from jquery.com-->
<!--script src="/media/javascript/jquery-1.4.4.js" type="text/javascript" charset="utf-8"></script-->